@gajae-code/coding-agent 0.15.4 → 0.15.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/dist/types/cli/update-cli.d.ts +25 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  4. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  5. package/dist/types/extensibility/shared-events.d.ts +2 -0
  6. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  7. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  8. package/dist/types/sdk/host/host.d.ts +2 -0
  9. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  10. package/dist/types/session/agent-session.d.ts +4 -2
  11. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  12. package/dist/types/tools/path-utils.d.ts +5 -6
  13. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  14. package/package.json +7 -7
  15. package/src/cli/update-cli.ts +102 -1
  16. package/src/commands/sdk.ts +5 -0
  17. package/src/config/autorouting-tier-map.ts +1 -0
  18. package/src/config/model-profiles.ts +5 -5
  19. package/src/config/model-registry.ts +10 -4
  20. package/src/coordinator-mcp/policy.ts +1 -1
  21. package/src/extensibility/extensions/types.ts +3 -0
  22. package/src/extensibility/shared-events.ts +2 -0
  23. package/src/gjc-runtime/launch-worktree.ts +44 -7
  24. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  25. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  26. package/src/internal-urls/docs-index.generated.ts +7 -7
  27. package/src/modes/prompt-action-autocomplete.ts +26 -47
  28. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  29. package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
  30. package/src/sdk/bus/prompt-reconciliation.ts +5 -2
  31. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  32. package/src/sdk/cli/session-cli.ts +149 -4
  33. package/src/sdk/host/host.ts +7 -0
  34. package/src/sdk/host/session-runtime.test.ts +821 -22
  35. package/src/sdk/host/session-runtime.ts +669 -67
  36. package/src/sdk/router/session-router.ts +69 -15
  37. package/src/session/agent-session.ts +232 -15
  38. package/src/session/internal/managed-session-storage.ts +64 -26
  39. package/src/session/session-manager.ts +26 -8
  40. package/src/tools/bash.ts +14 -0
  41. package/src/tools/path-utils.ts +77 -29
  42. package/src/tools/tmux-self-injection-guard.ts +359 -0
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  type AutocompleteItem,
3
3
  type AutocompleteProvider,
4
+ autocompleteFuzzyMatch,
5
+ autocompleteFuzzyScore,
4
6
  CombinedAutocompleteProvider,
5
7
  extractSlashCommandTokenPrefix,
6
8
  getSlashCommandMatchRank,
9
+ normalizeFuzzyText,
7
10
  type SlashCommand,
8
11
  } from "@gajae-code/tui";
9
12
  import type { KeybindingsManager } from "../config/keybindings";
@@ -50,43 +53,6 @@ interface PromptActionAutocompleteOptions {
50
53
  getPromptSuggestion?: () => string | null;
51
54
  }
52
55
 
53
- function fuzzyMatch(query: string, target: string): boolean {
54
- if (query.length === 0) return true;
55
- if (query.length > target.length) return false;
56
-
57
- let queryIndex = 0;
58
- for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
59
- if (query[queryIndex] === target[targetIndex]) {
60
- queryIndex += 1;
61
- }
62
- }
63
-
64
- return queryIndex === query.length;
65
- }
66
-
67
- function fuzzyScore(query: string, target: string): number {
68
- if (query.length === 0) return 1;
69
- if (target === query) return 100;
70
- if (target.startsWith(query)) return 80;
71
- if (target.includes(query)) return 60;
72
-
73
- let queryIndex = 0;
74
- let gaps = 0;
75
- let lastMatchIndex = -1;
76
- for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
77
- if (query[queryIndex] === target[targetIndex]) {
78
- if (lastMatchIndex >= 0 && targetIndex - lastMatchIndex > 1) {
79
- gaps += 1;
80
- }
81
- lastMatchIndex = targetIndex;
82
- queryIndex += 1;
83
- }
84
- }
85
-
86
- if (queryIndex !== query.length) return 0;
87
- return Math.max(1, 40 - gaps * 5);
88
- }
89
-
90
56
  function isPromptActionItem(item: AutocompleteItem): item is PromptActionAutocompleteItem {
91
57
  return "actionId" in item && "execute" in item && typeof item.execute === "function";
92
58
  }
@@ -127,6 +93,7 @@ function sortSlashCommandSuggestions(
127
93
  ): { items: AutocompleteItem[]; prefix: string } | null {
128
94
  if (!suggestions) return null;
129
95
  const query = suggestions.prefix.slice(1).toLowerCase();
96
+ const normalizedQuery = normalizeFuzzyText(query);
130
97
  const commandIndexes = new Map(commands.map((command, index) => [command.name, index]));
131
98
  const commandByName = new Map(commands.map(command => [command.name, command]));
132
99
  const items = suggestions.items
@@ -135,13 +102,20 @@ function sortSlashCommandSuggestions(
135
102
  const commandIndex = commandIndexes.get(item.value) ?? index;
136
103
  const lowerName = item.value.toLowerCase();
137
104
  const lowerDesc = command?.description?.toLowerCase() ?? item.description?.toLowerCase() ?? "";
138
- const nameScore = fuzzyMatch(query, lowerName) ? fuzzyScore(query, lowerName) : 0;
139
- const descScore = fuzzyMatch(query, lowerDesc) ? fuzzyScore(query, lowerDesc) * 0.5 : 0;
105
+ const normalizedName = normalizeFuzzyText(item.value);
106
+ const normalizedDesc = normalizeFuzzyText(lowerDesc);
107
+ const nameScore = autocompleteFuzzyMatch(normalizedQuery, normalizedName)
108
+ ? autocompleteFuzzyScore(normalizedQuery, normalizedName)
109
+ : 0;
110
+ const descScore = autocompleteFuzzyMatch(normalizedQuery, normalizedDesc)
111
+ ? autocompleteFuzzyScore(normalizedQuery, normalizedDesc) * 0.5
112
+ : 0;
113
+ const isAscii = /^[\x00-\x7F]*$/.test(`${query}${item.value}`);
140
114
  return {
141
115
  item,
142
116
  index,
143
117
  commandIndex,
144
- matchRank: getSlashCommandMatchRank(query, lowerName),
118
+ matchRank: isAscii ? getSlashCommandMatchRank(query, lowerName) : 4,
145
119
  priority: getSlashCommandPriority(command, item),
146
120
  score: Math.max(nameScore, descScore),
147
121
  };
@@ -219,14 +193,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
219
193
  const searchable = [action.id, action.label, action.description, ...action.keywords]
220
194
  .join(" ")
221
195
  .toLowerCase();
222
- if (!fuzzyMatch(query, searchable)) return null;
196
+ if (!autocompleteFuzzyMatch(query, searchable)) return null;
223
197
  return {
224
198
  value: action.label,
225
199
  label: action.label,
226
200
  description: action.description,
227
201
  actionId: action.id,
228
202
  execute: action.execute,
229
- score: fuzzyScore(query, searchable),
203
+ score: autocompleteFuzzyScore(query, searchable),
230
204
  } satisfies PromptActionAutocompleteItem & { score: number };
231
205
  })
232
206
  .filter(item => item !== null)
@@ -350,7 +324,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
350
324
  if (!prefix) return null;
351
325
  const query = prefix.slice(1).toLowerCase();
352
326
  if (query.length === 0 && !options.includeEmpty) return null;
353
- const normalizedQuery = query.startsWith("skill-") ? `skill:${query.slice("skill-".length)}` : query;
327
+ const normalizedQuery = normalizeFuzzyText(
328
+ query.startsWith("skill-") ? `skill:${query.slice("skill-".length)}` : query,
329
+ );
330
+ if (query.length > 0 && normalizedQuery.length === 0) return null;
354
331
  const exactNonSkillCommand = this.#commands.some(
355
332
  command => command.name === query && !command.name.startsWith("skill:"),
356
333
  );
@@ -364,16 +341,18 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
364
341
  `skill-${skillName}`,
365
342
  ...(exactNonSkillCommand ? [] : [skillName]),
366
343
  command.description ?? "",
367
- ].map(target => target.toLowerCase());
344
+ ].map(target => normalizeFuzzyText(target));
368
345
  if (
369
- !searchTargets.some(target => fuzzyMatch(normalizedQuery, target) || target.includes(normalizedQuery))
346
+ !searchTargets.some(
347
+ target => autocompleteFuzzyMatch(normalizedQuery, target) || target.includes(normalizedQuery),
348
+ )
370
349
  ) {
371
350
  return null;
372
351
  }
373
352
  const bestScore = Math.max(
374
353
  ...searchTargets.map(target =>
375
- fuzzyMatch(normalizedQuery, target)
376
- ? fuzzyScore(normalizedQuery, target)
354
+ autocompleteFuzzyMatch(normalizedQuery, target)
355
+ ? autocompleteFuzzyScore(normalizedQuery, target)
377
356
  : target.includes(normalizedQuery)
378
357
  ? 60
379
358
  : 0,
@@ -132,8 +132,8 @@ export type ChatDaemonAction = "stop" | "reload";
132
132
  * would retain the old hot polling loop, so replacement is required.
133
133
  */
134
134
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
135
- discord: 69,
136
- slack: 72,
135
+ discord: 70,
136
+ slack: 73,
137
137
  };
138
138
 
139
139
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -355,7 +355,13 @@ export function createKindAwareReconciliation(
355
355
  // as-is (cleanupRecords is intentionally not re-run). First reason wins: a
356
356
  // late generic frame never overwrites a specific one. Persisted so the reason
357
357
  // survives reconnect/restart reconciliation.
358
- if (frame.type === "agent_failed" && record.error === undefined) {
358
+ if (frame.type === "agent_failed") {
359
+ const failure = sanitizePromptFailure(frame.error);
360
+ if (
361
+ record.error !== undefined &&
362
+ !(record.error.code === "agent_failed" && failure.code !== "agent_failed")
363
+ )
364
+ return { value: undefined, changed: false };
359
365
  record.error = sanitizePromptFailure(frame.error);
360
366
  return { value: undefined, changed: true };
361
367
  }
@@ -373,7 +379,9 @@ export function createKindAwareReconciliation(
373
379
  if (frame.type === "agent_failed") {
374
380
  // agent_failed is additive diagnostics; agent_end remains the sole
375
381
  // terminal lifecycle boundary for the correlated invocation.
376
- record.error ??= sanitizePromptFailure(frame.error);
382
+ const failure = sanitizePromptFailure(frame.error);
383
+ if (record.error === undefined || (record.error.code === "agent_failed" && failure.code !== "agent_failed"))
384
+ record.error = failure;
377
385
  delete record.deadlineRecoveryPending;
378
386
  delete record.deadlineMaxAt;
379
387
  return { value: undefined, changed: true };
@@ -202,8 +202,11 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
202
202
  // the settled record instead of being dropped; it must not resurrect it,
203
203
  // so status, terminalAt, retention order, and clientRefIndex stay as-is.
204
204
  // First reason wins: a late generic frame never overwrites a specific one.
205
- if (frame.type === "agent_failed" && record.error === undefined)
206
- record.error = sanitizePromptFailure(frame.error);
205
+ if (frame.type === "agent_failed") {
206
+ const failure = sanitizePromptFailure(frame.error);
207
+ if (record.error === undefined || (record.error.code === "agent_failed" && failure.code !== "agent_failed"))
208
+ record.error = failure;
209
+ }
207
210
  return;
208
211
  }
209
212
  if (frame.type === "agent_start") {
@@ -283,8 +283,15 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
283
283
  * Generation 177 consumes an exact same-window lean settlement receipt only
284
284
  * after its autonomous ask lead-in publishes successfully, preventing idle
285
285
  * replay without changing the serving protocol.
286
+ * Generation 178: idle publication waits for positioned identity delivery to
287
+ * cross the native writer barrier before the independent broadcast lane.
288
+ * Generation 179 tolerates a bounded consecutive run of refused notification
289
+ * publications in the router instead of cancelling the subscription on the
290
+ * first refusal, so an owner still running the generation-178 router keeps
291
+ * killing a session's mirroring after one transient rejection and must be
292
+ * replaced across this upgrade.
286
293
  */
287
- export const DAEMON_GENERATION = 178;
294
+ export const DAEMON_GENERATION = 179;
288
295
 
289
296
  /**
290
297
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
@@ -1,7 +1,9 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import * as fsSync from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
4
5
  import { getAgentDir } from "@gajae-code/utils";
6
+ import { repo as resolveGitRepository } from "../../utils/git";
5
7
  import { ensureBroker } from "../broker/ensure";
6
8
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
7
9
  import { SdkClientError } from "../client";
@@ -38,10 +40,12 @@ export type SdkSessionCliAction =
38
40
  | "status"
39
41
  | "tail"
40
42
  | "retire"
43
+ | "global"
41
44
  | "raw"
42
45
  | "control"
43
46
  | "query"
44
47
  | "global";
48
+ export type SdkSessionListScope = "repo" | "cwd" | "worktree" | "all";
45
49
  export type SdkSessionCliRawKind = "control" | "query" | "global";
46
50
 
47
51
  export interface SdkSessionCliArgs {
@@ -56,6 +60,7 @@ export interface SdkSessionCliArgs {
56
60
  jsonInputFile?: string;
57
61
  jsonInputStdin?: boolean;
58
62
  idempotencyKey?: string;
63
+ scope?: string;
59
64
  confirm?: boolean;
60
65
  cursor?: string;
61
66
  wait?: boolean;
@@ -358,16 +363,156 @@ async function sessionRows(agentDir: string): Promise<SessionRows> {
358
363
  });
359
364
  }
360
365
 
361
- async function runList(agentDir: string): Promise<unknown> {
366
+ const SESSION_LIST_SCOPES: readonly SdkSessionListScope[] = ["repo", "cwd", "worktree", "all"];
367
+ /** Row workspaces whose locator cannot be canonicalized still get one deterministic identity. */
368
+ const WORKSPACE_IDENTITY_CACHE_LIMIT = 4096;
369
+
370
+ interface WorkspaceIdentity {
371
+ /** Canonical workspace path (realpath when it exists, lexical resolve otherwise). */
372
+ canonicalPath: string;
373
+ /** Canonical containing worktree root; null outside a Git checkout. */
374
+ repoRoot: string | null;
375
+ /** Canonical Git common dir; shared by the main checkout and linked worktrees. */
376
+ commonDir: string | null;
377
+ }
378
+
379
+ export interface SdkSessionListSelection {
380
+ scope: SdkSessionListScope;
381
+ selection: WorkspaceIdentity;
382
+ /** Bounded, credential-free descriptor included in list output. */
383
+ descriptor: {
384
+ scope: SdkSessionListScope;
385
+ path: string;
386
+ worktreeRoot?: string;
387
+ commonDir?: string;
388
+ };
389
+ }
390
+
391
+ /** Parses `--scope`; missing means `repo`, anything else invalid is a usage error. */
392
+ export function parseSessionListScope(value: string | undefined): SdkSessionListScope {
393
+ if (value === undefined) return "repo";
394
+ if ((SESSION_LIST_SCOPES as readonly string[]).includes(value)) return value as SdkSessionListScope;
395
+ throw new SdkSessionCliError(
396
+ "usage",
397
+ `Invalid scope "${value}". Expected one of: ${SESSION_LIST_SCOPES.join(", ")}.`,
398
+ 2,
399
+ );
400
+ }
401
+
402
+ async function canonicalWorkspacePath(target: string): Promise<string> {
403
+ try {
404
+ return await fs.realpath(target);
405
+ } catch {
406
+ // A removed or unreadable workspace keeps a deterministic lexical identity
407
+ // instead of guessing a broader or narrower one.
408
+ return path.resolve(target);
409
+ }
410
+ }
411
+
412
+ const workspaceIdentityCache = new Map<string, WorkspaceIdentity>();
413
+
414
+ /** Resolves (and caches) the Git identity of one workspace locator. */
415
+ async function workspaceIdentity(target: string): Promise<WorkspaceIdentity> {
416
+ const canonicalPath = await canonicalWorkspacePath(target);
417
+ const cached = workspaceIdentityCache.get(canonicalPath);
418
+ if (cached) return cached;
419
+ if (target === "unknown") {
420
+ const unavailable = { canonicalPath, repoRoot: null, commonDir: null };
421
+ workspaceIdentityCache.set(canonicalPath, unavailable);
422
+ return unavailable;
423
+ }
424
+ try {
425
+ if (!(await fs.stat(target)).isDirectory()) throw new Error("workspace is not a directory");
426
+ } catch {
427
+ // A removed or unreadable locator cannot prove Git membership. Keeping it
428
+ // outside every identity is the narrowest safe result for row filtering.
429
+ const unavailable = { canonicalPath, repoRoot: null, commonDir: null };
430
+ workspaceIdentityCache.set(canonicalPath, unavailable);
431
+ return unavailable;
432
+ }
433
+ const repository = await resolveGitRepository.resolve(canonicalPath);
434
+ const identity: WorkspaceIdentity = repository
435
+ ? {
436
+ canonicalPath,
437
+ repoRoot: await canonicalWorkspacePath(repository.repoRoot),
438
+ commonDir: await canonicalWorkspacePath(repository.commonDir),
439
+ }
440
+ : { canonicalPath, repoRoot: null, commonDir: null };
441
+ if (workspaceIdentityCache.size >= WORKSPACE_IDENTITY_CACHE_LIMIT) workspaceIdentityCache.clear();
442
+ workspaceIdentityCache.set(canonicalPath, identity);
443
+ return identity;
444
+ }
445
+
446
+ /**
447
+ * Resolves the list selection from `--repo` (default: process cwd) under the
448
+ * effective scope. Outside Git, `repo`/`worktree` fail typed and actionable —
449
+ * they never broaden to `all`; `cwd` remains an exact canonical match.
450
+ */
451
+ export async function resolveSessionListSelection(
452
+ scope: SdkSessionListScope,
453
+ repoArg: string | undefined,
454
+ ): Promise<SdkSessionListSelection> {
455
+ const selection = await workspaceIdentity(repoArg ?? process.cwd());
456
+ if (scope !== "cwd" && !selection.repoRoot)
457
+ throw new SdkSessionCliError(
458
+ "not_a_repository",
459
+ `--scope ${scope} requires a Git repository, but "${selection.canonicalPath}" is outside any Git checkout. ` +
460
+ "Use --scope cwd for an exact workspace match or --scope all for the full broker listing.",
461
+ 1,
462
+ );
463
+ const descriptor: SdkSessionListSelection["descriptor"] = {
464
+ scope,
465
+ path: selection.canonicalPath,
466
+ ...(selection.repoRoot ? { worktreeRoot: selection.repoRoot, commonDir: selection.commonDir! } : {}),
467
+ };
468
+ return { scope, selection, descriptor };
469
+ }
470
+
471
+ /**
472
+ * Filters fully traversed broker rows by the selection scope. Row workspaces
473
+ * are canonicalized and cached per distinct locator; `repo` matches the shared
474
+ * common dir across the main checkout and linked worktrees, `worktree` the
475
+ * containing worktree only, and `cwd` the exact canonical workspace.
476
+ */
477
+ export async function filterSessionRowsByScope(
478
+ rows: readonly SdkSessionRowV1[],
479
+ scope: SdkSessionListScope,
480
+ selection: SdkSessionListSelection,
481
+ ): Promise<{ sessions: SdkSessionRowV1[]; warnings: string[] }> {
482
+ if (scope === "all") return { sessions: [...rows], warnings: [] };
483
+ const warnings: string[] = [];
484
+ const sessions: SdkSessionRowV1[] = [];
485
+ for (const row of rows) {
486
+ const identity = await workspaceIdentity(row.locator.repo);
487
+ let keep: boolean;
488
+ if (scope === "cwd") keep = identity.canonicalPath === selection.selection.canonicalPath;
489
+ else if (scope === "worktree")
490
+ keep = identity.repoRoot !== null && identity.repoRoot === selection.selection.repoRoot;
491
+ else keep = identity.commonDir !== null && identity.commonDir === selection.selection.commonDir;
492
+ if (keep) sessions.push(row);
493
+ else if (identity.repoRoot === null && identity.commonDir === null)
494
+ warnings.push(
495
+ `Session ${row.sessionId} workspace "${row.locator.repo}" is outside Git; excluded by scope ${scope}.`,
496
+ );
497
+ }
498
+ return { sessions, warnings };
499
+ }
500
+
501
+ async function runList(agentDir: string, args: SdkSessionCliArgs): Promise<unknown> {
502
+ const scope = parseSessionListScope(args.scope);
503
+ const { selection, descriptor } = await resolveSessionListSelection(scope, args.repo);
362
504
  const listing = await sessionRows(agentDir);
505
+ const filtered = await filterSessionRowsByScope(listing.sessions, scope, { scope, selection, descriptor });
363
506
  return {
364
507
  ok: true,
365
508
  result: {
366
509
  version: SESSION_ROWS_VERSION,
367
510
  source: "broker",
511
+ scope,
512
+ selection: descriptor,
368
513
  ...(listing.indexSeq === undefined ? {} : { indexSeq: listing.indexSeq }),
369
- sessions: listing.sessions,
370
- warnings: listing.warnings,
514
+ sessions: filtered.sessions,
515
+ warnings: [...listing.warnings, ...filtered.warnings],
371
516
  },
372
517
  };
373
518
  }
@@ -1367,7 +1512,7 @@ export async function runSdkSessionCli(
1367
1512
  );
1368
1513
  const agentDir = args.agentDir ?? getAgentDir();
1369
1514
  if (action === "list") {
1370
- writeOutput(stripSecretFields(await runList(agentDir)));
1515
+ writeOutput(stripSecretFields(await runList(agentDir, args)));
1371
1516
  return;
1372
1517
  }
1373
1518
  if (action === "inspect") {
@@ -49,6 +49,8 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
49
49
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
50
50
  /** Test/lifecycle seam invoked synchronously before fire-and-forget dispatch. */
51
51
  onFrameAdmitted?: (connectionId: string, frame: SdkFrame) => void;
52
+ /** Synchronously rejects external input while a runtime is being replaced. */
53
+ onFrameAdmission?: (connectionId: string, frame: SdkFrame) => SdkFrame | undefined;
52
54
  /** Best-effort diagnostic observation of accepted control/query frames. */
53
55
  onRequest?: SdkRequestObserver;
54
56
  /** Runs before a control response is sent; identity transitions use sendTerminal. */
@@ -408,6 +410,11 @@ export class SessionSdkHost {
408
410
 
409
411
  async #onFrame(connectionId: string, frame: SdkFrame): Promise<void> {
410
412
  try {
413
+ const admissionRejection = this.#options.onFrameAdmission?.(connectionId, frame);
414
+ if (admissionRejection !== undefined) {
415
+ await this.#send(connectionId, admissionRejection);
416
+ return;
417
+ }
411
418
  switch (frame.type) {
412
419
  case "control_request": {
413
420
  this.#observeRequest("control", connectionId, frame);