@animalabs/agent-framework 0.30.0 → 0.32.0

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 (189) hide show
  1. package/README.md +71 -8
  2. package/dist/src/agent.d.ts +29 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +82 -17
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  7. package/dist/src/api/mcp-server-core.js +10 -3
  8. package/dist/src/api/mcp-server-core.js.map +1 -1
  9. package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
  10. package/dist/src/code-execution/isolate/render.js +7 -6
  11. package/dist/src/code-execution/isolate/render.js.map +1 -1
  12. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  13. package/dist/src/code-execution/isolate/scripts.js +5 -4
  14. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  15. package/dist/src/framework.d.ts +138 -102
  16. package/dist/src/framework.d.ts.map +1 -1
  17. package/dist/src/framework.js +1133 -752
  18. package/dist/src/framework.js.map +1 -1
  19. package/dist/src/gate/event-gate.d.ts +24 -17
  20. package/dist/src/gate/event-gate.d.ts.map +1 -1
  21. package/dist/src/gate/event-gate.js +198 -144
  22. package/dist/src/gate/event-gate.js.map +1 -1
  23. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
  24. package/dist/src/gate/recipe-reconcile.js +6 -5
  25. package/dist/src/gate/recipe-reconcile.js.map +1 -1
  26. package/dist/src/host-presentation.d.ts.map +1 -1
  27. package/dist/src/host-presentation.js +5 -7
  28. package/dist/src/host-presentation.js.map +1 -1
  29. package/dist/src/index.d.ts +1 -0
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +1 -0
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/inference-bounds.d.ts +37 -0
  34. package/dist/src/inference-bounds.d.ts.map +1 -0
  35. package/dist/src/inference-bounds.js +84 -0
  36. package/dist/src/inference-bounds.js.map +1 -0
  37. package/dist/src/line-reader.d.ts +7 -1
  38. package/dist/src/line-reader.d.ts.map +1 -1
  39. package/dist/src/line-reader.js +17 -6
  40. package/dist/src/line-reader.js.map +1 -1
  41. package/dist/src/mcpl/errors.d.ts +1 -0
  42. package/dist/src/mcpl/errors.d.ts.map +1 -1
  43. package/dist/src/mcpl/errors.js +3 -0
  44. package/dist/src/mcpl/errors.js.map +1 -1
  45. package/dist/src/mcpl/inbound-source.d.ts +2 -2
  46. package/dist/src/mcpl/inbound-source.js +2 -2
  47. package/dist/src/mcpl/inference-router.d.ts +11 -1
  48. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  49. package/dist/src/mcpl/inference-router.js +65 -9
  50. package/dist/src/mcpl/inference-router.js.map +1 -1
  51. package/dist/src/mcpl/modern-connection.d.ts +10 -7
  52. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  53. package/dist/src/mcpl/modern-connection.js +85 -74
  54. package/dist/src/mcpl/modern-connection.js.map +1 -1
  55. package/dist/src/mcpl/protocol-family.d.ts +1 -1
  56. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  57. package/dist/src/mcpl/protocol-family.js +45 -3
  58. package/dist/src/mcpl/protocol-family.js.map +1 -1
  59. package/dist/src/mcpl/push-coalescer.d.ts +2 -7
  60. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  61. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  62. package/dist/src/mcpl/references.d.ts +1 -2
  63. package/dist/src/mcpl/references.d.ts.map +1 -1
  64. package/dist/src/mcpl/references.js +3 -3
  65. package/dist/src/mcpl/references.js.map +1 -1
  66. package/dist/src/mcpl/server-connection.d.ts +14 -0
  67. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  68. package/dist/src/mcpl/server-connection.js +26 -22
  69. package/dist/src/mcpl/server-connection.js.map +1 -1
  70. package/dist/src/mcpl/tool-signature.d.ts +1 -0
  71. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  72. package/dist/src/mcpl/tool-signature.js +16 -11
  73. package/dist/src/mcpl/tool-signature.js.map +1 -1
  74. package/dist/src/mcpl/transport.d.ts +44 -21
  75. package/dist/src/mcpl/transport.d.ts.map +1 -1
  76. package/dist/src/mcpl/transport.js +283 -64
  77. package/dist/src/mcpl/transport.js.map +1 -1
  78. package/dist/src/mcpl/types.d.ts +17 -1
  79. package/dist/src/mcpl/types.d.ts.map +1 -1
  80. package/dist/src/mcpl/types.js.map +1 -1
  81. package/dist/src/module-registry.d.ts +32 -4
  82. package/dist/src/module-registry.d.ts.map +1 -1
  83. package/dist/src/module-registry.js +189 -25
  84. package/dist/src/module-registry.js.map +1 -1
  85. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  86. package/dist/src/modules/discord/discord-js-client.js +24 -16
  87. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  88. package/dist/src/modules/sender-name.d.ts +5 -4
  89. package/dist/src/modules/sender-name.d.ts.map +1 -1
  90. package/dist/src/modules/sender-name.js +7 -5
  91. package/dist/src/modules/sender-name.js.map +1 -1
  92. package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
  93. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  94. package/dist/src/modules/workspace/disk-agreement.js +33 -12
  95. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  96. package/dist/src/modules/workspace/index.d.ts +6 -3
  97. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  98. package/dist/src/modules/workspace/index.js +61 -15
  99. package/dist/src/modules/workspace/index.js.map +1 -1
  100. package/dist/src/modules/workspace/observe.d.ts +9 -1
  101. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  102. package/dist/src/modules/workspace/observe.js +50 -10
  103. package/dist/src/modules/workspace/observe.js.map +1 -1
  104. package/dist/src/modules/workspace/reconcile.d.ts +11 -1
  105. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  106. package/dist/src/modules/workspace/reconcile.js +19 -8
  107. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  108. package/dist/src/modules/workspace/types.d.ts +8 -0
  109. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  110. package/dist/src/modules/workspace/types.js.map +1 -1
  111. package/dist/src/modules/workspace/watcher.d.ts +7 -0
  112. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  113. package/dist/src/modules/workspace/watcher.js +7 -5
  114. package/dist/src/modules/workspace/watcher.js.map +1 -1
  115. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  116. package/dist/src/recovery/discord-awareness-outbox.js +1 -9
  117. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  118. package/dist/src/supplied-text.d.ts +2 -0
  119. package/dist/src/supplied-text.d.ts.map +1 -1
  120. package/dist/src/supplied-text.js +16 -7
  121. package/dist/src/supplied-text.js.map +1 -1
  122. package/dist/src/tool-result-guard.js +1 -1
  123. package/dist/src/tool-result-guard.js.map +1 -1
  124. package/dist/src/tune-out/coordinator.d.ts +2 -20
  125. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  126. package/dist/src/tune-out/coordinator.js +175 -70
  127. package/dist/src/tune-out/coordinator.js.map +1 -1
  128. package/dist/src/tune-out/tools.d.ts +14 -2
  129. package/dist/src/tune-out/tools.d.ts.map +1 -1
  130. package/dist/src/tune-out/tools.js.map +1 -1
  131. package/dist/src/types/agent.d.ts +6 -12
  132. package/dist/src/types/agent.d.ts.map +1 -1
  133. package/dist/src/types/framework.d.ts +65 -24
  134. package/dist/src/types/framework.d.ts.map +1 -1
  135. package/dist/src/types/module.d.ts +42 -3
  136. package/dist/src/types/module.d.ts.map +1 -1
  137. package/dist/src/types/trace.d.ts +4 -3
  138. package/dist/src/types/trace.d.ts.map +1 -1
  139. package/package.json +3 -3
  140. package/src/agent.ts +90 -25
  141. package/src/api/mcp-server-core.ts +10 -3
  142. package/src/code-execution/isolate/render.ts +7 -6
  143. package/src/code-execution/isolate/scripts.ts +5 -4
  144. package/src/framework.ts +1163 -770
  145. package/src/gate/event-gate.ts +220 -145
  146. package/src/gate/recipe-reconcile.ts +7 -5
  147. package/src/host-presentation.ts +4 -7
  148. package/src/index.ts +1 -0
  149. package/src/inference-bounds.ts +111 -0
  150. package/src/line-reader.ts +22 -5
  151. package/src/mcpl/errors.ts +4 -0
  152. package/src/mcpl/inbound-source.ts +2 -2
  153. package/src/mcpl/inference-router.ts +67 -10
  154. package/src/mcpl/modern-connection.ts +83 -68
  155. package/src/mcpl/protocol-family.ts +37 -4
  156. package/src/mcpl/push-coalescer.ts +2 -7
  157. package/src/mcpl/references.ts +3 -4
  158. package/src/mcpl/server-connection.ts +30 -21
  159. package/src/mcpl/tool-signature.ts +18 -11
  160. package/src/mcpl/transport.ts +261 -60
  161. package/src/mcpl/types.ts +18 -1
  162. package/src/module-registry.ts +178 -25
  163. package/src/modules/discord/discord-js-client.ts +33 -29
  164. package/src/modules/sender-name.ts +8 -5
  165. package/src/modules/workspace/disk-agreement.ts +39 -15
  166. package/src/modules/workspace/index.ts +65 -14
  167. package/src/modules/workspace/observe.ts +51 -10
  168. package/src/modules/workspace/reconcile.ts +22 -8
  169. package/src/modules/workspace/types.ts +5 -0
  170. package/src/modules/workspace/watcher.ts +8 -6
  171. package/src/recovery/discord-awareness-outbox.ts +1 -11
  172. package/src/supplied-text.ts +17 -6
  173. package/src/tool-result-guard.ts +1 -1
  174. package/src/tune-out/coordinator.ts +187 -85
  175. package/src/tune-out/tools.ts +14 -2
  176. package/src/types/agent.ts +6 -12
  177. package/src/types/framework.ts +66 -24
  178. package/src/types/module.ts +42 -3
  179. package/src/types/trace.ts +4 -3
  180. package/dist/src/reasoning-loss-notice.d.ts +0 -23
  181. package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
  182. package/dist/src/reasoning-loss-notice.js +0 -52
  183. package/dist/src/reasoning-loss-notice.js.map +0 -1
  184. package/dist/src/reasoning-loss.d.ts +0 -24
  185. package/dist/src/reasoning-loss.d.ts.map +0 -1
  186. package/dist/src/reasoning-loss.js +0 -102
  187. package/dist/src/reasoning-loss.js.map +0 -1
  188. package/src/reasoning-loss-notice.ts +0 -64
  189. package/src/reasoning-loss.ts +0 -120
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { NOTICE_TAG } from '../notice-tags.js';
15
- import { cutToFit, suppliedValue } from '../supplied-text.js';
15
+ import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-text.js';
16
16
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
17
  import { basename, dirname, join } from 'node:path';
18
18
  import { GateScript } from './gate-script.js';
@@ -483,10 +483,10 @@ function beforeWriting(err: unknown): boolean {
483
483
  * "policies" that isn't a list and holds something. `runs` is what the gate
484
484
  * runs meanwhile (rulesRun).
485
485
  */
486
- function noListRefusal(noList: string, runs: string): string {
486
+ function noListRefusal(noList: string, runs: string, path: string): string {
487
487
  return `${noList}, so it has no list of rules a change could edit without writing over what it holds, and nothing `
488
- + `was saved. Meanwhile the gate runs ${runs}. Until gate.json is an object whose "policies" is a list holding its `
489
- + 'rules, a change is refused the same way.';
488
+ + `was saved. Meanwhile the gate runs ${runs}. Until gate.json, at ${path}, is an object whose "policies" is a list `
489
+ + 'holding its rules, a change is refused the same way.';
490
490
  }
491
491
 
492
492
  /**
@@ -495,15 +495,15 @@ function noListRefusal(noList: string, runs: string): string {
495
495
  * written over it, so what it holds, often an operator's unfinished edit, stays
496
496
  * as it is. `runs` is what the gate runs meanwhile (rulesRun).
497
497
  */
498
- function unusableRefusal(unusable: { error: string; notJson?: true }, runs: string): string {
498
+ function unusableRefusal(unusable: { error: string; notJson?: true }, runs: string, path: string): string {
499
499
  if (!unusable.notJson) {
500
- return `gate.json couldn't be read (${unusable.error}), so a change can't edit it, and nothing was written over it. `
501
- + `Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json readable; until then, a change is `
502
- + 'refused the same way.';
500
+ return `gate.json, at ${path}, couldn't be read (${unusable.error}), so a change can't edit it, and nothing was written `
501
+ + `over it. Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json a file the gate can read; `
502
+ + 'until then, a change is refused the same way.';
503
503
  }
504
- return `gate.json isn't JSON the gate can read (${unusable.error}), so a change can't edit its rules without writing `
505
- + `over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change is `
506
- + 'refused the same way.';
504
+ return `gate.json, at ${path}, isn't JSON the gate can read (${unusable.error}), so a change can't edit its rules without `
505
+ + `writing over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change `
506
+ + 'is refused the same way.';
507
507
  }
508
508
 
509
509
  /**
@@ -514,13 +514,19 @@ function unusableRefusal(unusable: { error: string; notJson?: true }, runs: stri
514
514
  * (notUsedClause, describeStart) say which rules keep gate.json out of use
515
515
  * and each fix.
516
516
  */
517
- function notUsedRefusal(runs: string): string {
518
- return "gate.json isn't used at this start, which runs as one without it does, and a change would put gate.json's rules in "
517
+ function notUsedRefusal(runs: string, path: string): string {
518
+ return `gate.json, at ${path}, isn't used at this start, which runs as one without it does, and a change would put gate.json's rules in `
519
519
  + `force in place of what that runs, so nothing was saved. Meanwhile the gate runs ${runs}. gate_status says what keeps `
520
520
  + 'gate.json out of use and what would put it in use, such as giving it rules under "policies", or fixing the starting '
521
521
  + 'rules, which whoever runs this host can do, for the next start; until then, a change is refused the same way.';
522
522
  }
523
523
 
524
+ function savedOnlyInForce(path: string, remedy: string | undefined): string {
525
+ return remedy === undefined
526
+ ? ''
527
+ : ` gate.json, at ${path}, isn't in force, and a change is saved only if it brings it into force. ${remedy}`;
528
+ }
529
+
524
530
  /**
525
531
  * A value as a message quotes it: its JSON, cut short when long, never inside a
526
532
  * surrogate pair, since the message reaches the agent's context.
@@ -531,7 +537,8 @@ export function shownValue(value: unknown): string {
531
537
  if (typeof value === 'number' && !Number.isFinite(value)) return String(value);
532
538
  let json: string;
533
539
  try {
534
- json = JSON.stringify(value) ?? String(value);
540
+ const stringified = JSON.stringify(value);
541
+ json = stringified === undefined ? String(value) : quotedJson(stringified);
535
542
  } catch {
536
543
  json = String(value);
537
544
  }
@@ -548,9 +555,22 @@ function joinedList(items: readonly string[], joiner: 'and' | 'or' = 'and'): str
548
555
  return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} ${joiner} ${items[items.length - 1]}`;
549
556
  }
550
557
 
558
+ const asGateNames = (path: string): string => path;
559
+
560
+ function namedBy(names: Readonly<Record<string, string>> | undefined): (path: string) => string {
561
+ if (!names) return asGateNames;
562
+ return (path) => {
563
+ for (let end = path.length; end > 0; end = path.lastIndexOf('.', end - 1)) {
564
+ const head = path.slice(0, end);
565
+ if (Object.hasOwn(names, head)) return `${names[head]}${path.slice(end)}`;
566
+ }
567
+ return path;
568
+ };
569
+ }
570
+
551
571
  /** `"a"`, `"a" and "b"`, `"a", "b" and "c"` (or "or"). */
552
572
  function quotedList(names: readonly string[], joiner: 'and' | 'or' = 'and'): string {
553
- return joinedList(names.map((n) => `"${n}"`), joiner);
573
+ return joinedList(names.map((n) => quotedValue(n)), joiner);
554
574
  }
555
575
 
556
576
  /** 1st, 2nd, 3rd, 4th, 11th, 21st. */
@@ -611,7 +631,7 @@ function fieldNames(
611
631
 
612
632
  /** `"tags" (did you mean "tagsAny", "tagsAll" or "tagsNone"?)` */
613
633
  function slipShown(slip: Slip): string {
614
- return `"${slip.key}" (did you mean ${quotedList(slip.meant, 'or')}?)`;
634
+ return `${quotedValue(slip.key)} (did you mean ${quotedList(slip.meant, 'or')}?)`;
615
635
  }
616
636
 
617
637
  /** Names an object has that are fields read at another level, with where that is (`homes`). */
@@ -651,22 +671,22 @@ function misplacedShown(m: Misplaced): string {
651
671
  * The levels of a gate.json the gate reads fields at, each as a message names
652
672
  * it, `file` naming the file itself.
653
673
  */
654
- function fieldLevels(file: string): ReadonlyArray<{ name: string; fields: readonly string[] }> {
674
+ function fieldLevels(file: string, named: (path: string) => string = asGateNames): ReadonlyArray<{ name: string; fields: readonly string[] }> {
655
675
  return [
656
676
  { name: file, fields: CONFIG_FIELDS },
657
677
  { name: 'a rule', fields: POLICY_FIELDS },
658
678
  { name: '"match"', fields: MATCH_FIELDS },
659
679
  { name: '"match.filter"', fields: FILTER_FIELDS },
660
680
  { name: '"behavior"', fields: BEHAVIOR_KINDS },
661
- { name: '"behavior.rate_limit"', fields: RATE_LIMIT_FIELDS },
662
- { name: '"behavior.passive_sample"', fields: PASSIVE_SAMPLE_FIELDS },
663
- { name: '"behavior.judge"', fields: JUDGE_FIELDS },
681
+ { name: `"${named('behavior.rate_limit')}"`, fields: RATE_LIMIT_FIELDS },
682
+ { name: `"${named('behavior.passive_sample')}"`, fields: PASSIVE_SAMPLE_FIELDS },
683
+ { name: `"${named('behavior.judge')}"`, fields: JUDGE_FIELDS },
664
684
  ];
665
685
  }
666
686
 
667
687
  /** Where a name is a field the gate reads, at a level other than the one whose fields are `here`. */
668
- function homesOf(file: string, here: readonly string[]): (key: string) => string[] {
669
- return (key) => fieldLevels(file)
688
+ function homesOf(file: string, here: readonly string[], named?: (path: string) => string): (key: string) => string[] {
689
+ return (key) => fieldLevels(file, named)
670
690
  .filter((level) => level.fields !== here && level.fields.includes(key))
671
691
  .map((level) => level.name);
672
692
  }
@@ -798,7 +818,7 @@ interface RuleProblem {
798
818
 
799
819
  /** A rule as the gate reads it. */
800
820
  interface RuleReading {
801
- /** How messages name it: `Policy "x"`, or its place when it has no name. */
821
+ /** How messages name it: `Rule "x"`, or its place when it has no name. */
802
822
  where: string;
803
823
  /** The rule as read, unless a problem has no reading. */
804
824
  policy?: GatePolicy;
@@ -833,6 +853,7 @@ interface Reader {
833
853
  where: string;
834
854
  /** How messages name the file the rule is in, for a field of the file's own put in the rule. */
835
855
  file: string;
856
+ named: (path: string) => string;
836
857
  problems: RuleProblem[];
837
858
  notes: string[];
838
859
  /** Set where its rate limit's tokens are a number not over 0, so it wakes on none of what it matches. */
@@ -859,14 +880,15 @@ function readFieldNames(
859
880
  misplacedReadOf: (m: Misplaced) => string | undefined = () => undefined,
860
881
  ): void {
861
882
  const { slips, unread } = fieldNames(obj, fields);
862
- const { misplaced, rest } = placedElsewhere(unread, homesOf(r.file, fields));
863
- noteUnread(level ? `${r.where}: ${level}` : r.where, rest, fields, r.notes);
883
+ const { misplaced, rest } = placedElsewhere(unread, homesOf(r.file, fields, r.named));
884
+ const shown = level === null ? null : r.named(level);
885
+ noteUnread(shown ? `${r.where}: ${shown}` : r.where, rest, fields, r.notes);
864
886
  for (const slip of slips) {
865
- r.problems.push({ text: `${level ?? 'it'} has ${slipShown(slip)}, which the gate doesn't read`, read: readOf(slip) });
887
+ r.problems.push({ text: `${shown ?? 'it'} has ${slipShown(slip)}, which the gate doesn't read`, read: readOf(slip) });
866
888
  }
867
889
  for (const m of misplaced) {
868
890
  r.problems.push({
869
- text: `${level ?? 'it'} has ${misplacedShown(m)}, which the gate doesn't read here`,
891
+ text: `${shown ?? 'it'} has ${misplacedShown(m)}, which the gate doesn't read here`,
870
892
  read: misplacedReadOf(m) ?? (m.keys.length === 1 ? SLIP_READ : 'so they do nothing'),
871
893
  });
872
894
  }
@@ -881,7 +903,7 @@ function droppedEntries(count: number): string {
881
903
  * A rule, read as written, or past each problem it has. `unnamed` names a rule
882
904
  * without a name in messages, and `file` the file it's in.
883
905
  */
884
- function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReading {
906
+ function readRule(raw: unknown, unnamed: string, file = 'gate.json', named: (path: string) => string = asGateNames): RuleReading {
885
907
  if (!isPlainObject(raw)) {
886
908
  return {
887
909
  where: unnamed,
@@ -891,7 +913,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReadin
891
913
  };
892
914
  }
893
915
  const name = nameOf(raw);
894
- const r: Reader = { where: name ? `Policy "${name}"` : unnamed, file, problems: [], notes: [] };
916
+ const r: Reader = { where: name ? `Rule ${quotedValue(name)}` : unnamed, file, named, problems: [], notes: [] };
895
917
  const obj = withoutEmpty(r.where, raw, r.notes, RULE_LEFT_OUT);
896
918
  // Field names first, so a slip of one is named before what its absence does.
897
919
  readFieldNames(r, null, obj, POLICY_FIELDS, (slip) => {
@@ -914,7 +936,7 @@ function readRule(raw: unknown, unnamed: string, file = 'gate.json'): RuleReadin
914
936
  const { behavior, kind } = readBehavior(r, obj.behavior);
915
937
  const match = readMatch(r, obj.match);
916
938
  const resets = readResets(r, obj.resets);
917
- const passthrough = readPassthrough(r, obj.passthrough, behavior, kind);
939
+ const passthrough = readPassthrough(r, obj.passthrough, kind);
918
940
  const reading: RuleReading = {
919
941
  where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
920
942
  };
@@ -969,7 +991,7 @@ function readBehavior(r: Reader, value: unknown): { behavior?: GateBehavior | un
969
991
  }
970
992
  const behavior = READ_KIND[kind](r, b[kind]);
971
993
  for (const other of kinds.slice(1)) {
972
- const unread: Reader = { where: r.where, file: r.file, problems: [], notes: [] };
994
+ const unread: Reader = { where: r.where, file: r.file, named: r.named, problems: [], notes: [] };
973
995
  READ_KIND[other](unread, b[other]);
974
996
  for (const p of unread.problems) r.problems.push({ text: p.text, read: `which goes unread with the rest of "${other}"` });
975
997
  }
@@ -985,18 +1007,18 @@ function readBehavior(r: Reader, value: unknown): { behavior?: GateBehavior | un
985
1007
  */
986
1008
  function readDebounce(r: Reader, ms: unknown): GateBehavior | undefined {
987
1009
  if (typeof ms !== 'number' || !Number.isFinite(ms)) {
988
- r.problems.push({ text: `behavior.debounce is ${shownValue(ms)}, where a number of milliseconds is needed, such as 60000` });
1010
+ r.problems.push({ text: `${r.named('behavior.debounce')} is ${shownValue(ms)}, where a number of milliseconds is needed, such as 60000` });
989
1011
  return undefined;
990
1012
  }
991
1013
  if (ms < MIN_DEBOUNCE_MS) {
992
1014
  r.problems.push({
993
- text: `behavior.debounce is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
1015
+ text: `${r.named('behavior.debounce')} is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
994
1016
  });
995
1017
  return { debounce: MIN_DEBOUNCE_MS };
996
1018
  }
997
1019
  if (ms > MAX_DEBOUNCE_MS) {
998
1020
  r.problems.push({
999
- text: `behavior.debounce is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
1021
+ text: `${r.named('behavior.debounce')} is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
1000
1022
  });
1001
1023
  return { debounce: MAX_DEBOUNCE_MS };
1002
1024
  }
@@ -1019,27 +1041,27 @@ function readDebounce(r: Reader, ms: unknown): GateBehavior | undefined {
1019
1041
  function readRateLimit(r: Reader, value: unknown): GateBehavior | undefined {
1020
1042
  if (!isPlainObject(value)) {
1021
1043
  r.problems.push({
1022
- text: `behavior.rate_limit is ${shownValue(value)}, where an object is needed, such as {"tokens": 3, "refillIntervalMs": 60000}`,
1044
+ text: `${r.named('behavior.rate_limit')} is ${shownValue(value)}, where an object is needed, such as {"tokens": 3, "refillIntervalMs": 60000}`,
1023
1045
  });
1024
1046
  return undefined;
1025
1047
  }
1026
- const conf = withoutEmpty(`${r.where}: behavior.rate_limit`, value, r.notes, { keyBy: 'every event the rule matches shares one bucket' });
1048
+ const conf = withoutEmpty(`${r.where}: ${r.named('behavior.rate_limit')}`, value, r.notes, { keyBy: 'every event the rule matches shares one bucket' });
1027
1049
  readFieldNames(r, 'behavior.rate_limit', conf, RATE_LIMIT_FIELDS);
1028
1050
  const tokens = conf.tokens;
1029
1051
  const refill = conf.refillIntervalMs;
1030
1052
  const tokensUsable = typeof tokens === 'number' && Number.isFinite(tokens) && tokens > 0;
1031
1053
  if (typeof tokens === 'number' && !(tokens > 0)) r.noTokens = true;
1032
1054
  if (!tokensUsable) {
1033
- r.problems.push({ text: `behavior.rate_limit.tokens is ${shownValue(tokens)}, where a positive number is needed` });
1055
+ r.problems.push({ text: `${r.named('behavior.rate_limit.tokens')} is ${shownValue(tokens)}, where a positive number is needed` });
1034
1056
  }
1035
1057
  const refillUsable = typeof refill === 'number' && Number.isFinite(refill);
1036
1058
  if (!refillUsable) {
1037
1059
  r.problems.push({
1038
- text: `behavior.rate_limit.refillIntervalMs is ${shownValue(refill)}, where a positive number of milliseconds is needed`,
1060
+ text: `${r.named('behavior.rate_limit.refillIntervalMs')} is ${shownValue(refill)}, where a positive number of milliseconds is needed`,
1039
1061
  });
1040
1062
  } else if (refill <= 0) {
1041
1063
  r.problems.push({
1042
- text: `behavior.rate_limit.refillIntervalMs is ${refill}, where a positive number of milliseconds is needed`,
1064
+ text: `${r.named('behavior.rate_limit.refillIntervalMs')} is ${refill}, where a positive number of milliseconds is needed`,
1043
1065
  read: refill === 0
1044
1066
  ? 'so its tokens refill at once, and it limits nothing it matches'
1045
1067
  : "so its tokens never refill, and once they're spent it wakes the agent on nothing it matches",
@@ -1059,21 +1081,21 @@ function readRateLimit(r: Reader, value: unknown): GateBehavior | undefined {
1059
1081
  */
1060
1082
  function readPassiveSample(r: Reader, value: unknown): GateBehavior | undefined {
1061
1083
  if (!isPlainObject(value)) {
1062
- r.problems.push({ text: `behavior.passive_sample is ${shownValue(value)}, where an object is needed, such as {"every": 10}` });
1084
+ r.problems.push({ text: `${r.named('behavior.passive_sample')} is ${shownValue(value)}, where an object is needed, such as {"every": 10}` });
1063
1085
  return undefined;
1064
1086
  }
1065
- const conf = withoutEmpty(`${r.where}: behavior.passive_sample`, value, r.notes, { keyBy: 'every event the rule matches shares one count' });
1087
+ const conf = withoutEmpty(`${r.where}: ${r.named('behavior.passive_sample')}`, value, r.notes, { keyBy: 'every event the rule matches shares one count' });
1066
1088
  readFieldNames(r, 'behavior.passive_sample', conf, PASSIVE_SAMPLE_FIELDS);
1067
1089
  const given = conf.every;
1068
1090
  let every: number | undefined;
1069
1091
  if (typeof given !== 'number' || !Number.isFinite(given)) {
1070
- r.problems.push({ text: `behavior.passive_sample.every is ${shownValue(given)}, where a whole number of 1 or more is needed` });
1092
+ r.problems.push({ text: `${r.named('behavior.passive_sample.every')} is ${shownValue(given)}, where a whole number of 1 or more is needed` });
1071
1093
  } else if (Number.isInteger(given) && given > 0) {
1072
1094
  every = given;
1073
1095
  } else {
1074
1096
  every = given <= 0 ? 1 : Math.ceil(given);
1075
1097
  r.problems.push({
1076
- text: `behavior.passive_sample.every is ${given}, ${given <= 0 ? 'under 1' : 'not a whole number'}`,
1098
+ text: `${r.named('behavior.passive_sample.every')} is ${given}, ${given <= 0 ? 'under 1' : 'not a whole number'}`,
1077
1099
  read: `so it's read as ${every}`,
1078
1100
  });
1079
1101
  }
@@ -1096,11 +1118,11 @@ function readPassiveSample(r: Reader, value: unknown): GateBehavior | undefined
1096
1118
  function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
1097
1119
  if (!isPlainObject(value)) {
1098
1120
  r.problems.push({
1099
- text: `behavior.judge is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
1121
+ text: `${r.named('behavior.judge')} is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
1100
1122
  });
1101
1123
  return undefined;
1102
1124
  }
1103
- const conf = withoutEmpty(`${r.where}: behavior.judge`, value, r.notes, {
1125
+ const conf = withoutEmpty(`${r.where}: ${r.named('behavior.judge')}`, value, r.notes, {
1104
1126
  maxWaitMs: 'a batch is judged only after its quiet period',
1105
1127
  instructions: 'the judge decides without the agent\'s word',
1106
1128
  });
@@ -1109,16 +1131,16 @@ function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
1109
1131
  const ms = conf.debounceMs;
1110
1132
  let debounceMs: number | undefined;
1111
1133
  if (typeof ms !== 'number' || !Number.isFinite(ms)) {
1112
- r.problems.push({ text: `behavior.judge.debounceMs is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
1134
+ r.problems.push({ text: `${r.named('behavior.judge.debounceMs')} is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
1113
1135
  ok = false;
1114
1136
  } else if (ms < MIN_DEBOUNCE_MS) {
1115
1137
  r.problems.push({
1116
- text: `behavior.judge.debounceMs is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
1138
+ text: `${r.named('behavior.judge.debounceMs')} is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
1117
1139
  });
1118
1140
  debounceMs = MIN_DEBOUNCE_MS;
1119
1141
  } else if (ms > MAX_DEBOUNCE_MS) {
1120
1142
  r.problems.push({
1121
- text: `behavior.judge.debounceMs is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
1143
+ text: `${r.named('behavior.judge.debounceMs')} is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
1122
1144
  });
1123
1145
  debounceMs = MAX_DEBOUNCE_MS;
1124
1146
  } else {
@@ -1128,19 +1150,19 @@ function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
1128
1150
  const wait = conf.maxWaitMs;
1129
1151
  if (wait !== undefined) {
1130
1152
  if (typeof wait !== 'number' || !Number.isFinite(wait)) {
1131
- r.problems.push({ text: `behavior.judge.maxWaitMs is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
1153
+ r.problems.push({ text: `${r.named('behavior.judge.maxWaitMs')} is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
1132
1154
  ok = false;
1133
1155
  } else {
1134
1156
  const floor = debounceMs ?? MIN_DEBOUNCE_MS;
1135
1157
  if (wait < floor) {
1136
1158
  r.problems.push({
1137
- text: `behavior.judge.maxWaitMs is ${wait} ms, under the rule's ${floor} ms quiet period`,
1159
+ text: `${r.named('behavior.judge.maxWaitMs')} is ${wait} ms, under the rule's ${floor} ms quiet period`,
1138
1160
  read: `so it's read as ${floor} ms`,
1139
1161
  });
1140
1162
  maxWaitMs = floor;
1141
1163
  } else if (wait > MAX_JUDGE_WAIT_MS) {
1142
1164
  r.problems.push({
1143
- text: `behavior.judge.maxWaitMs is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
1165
+ text: `${r.named('behavior.judge.maxWaitMs')} is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
1144
1166
  read: `so it's read as ${MAX_JUDGE_WAIT_MS} ms`,
1145
1167
  });
1146
1168
  maxWaitMs = MAX_JUDGE_WAIT_MS;
@@ -1153,11 +1175,11 @@ function readJudge(r: Reader, value: unknown): GateBehavior | undefined {
1153
1175
  let instructions: string | undefined;
1154
1176
  if (said !== undefined) {
1155
1177
  if (typeof said !== 'string') {
1156
- r.problems.push({ text: `behavior.judge.instructions is ${shownValue(said)}, where text is needed` });
1178
+ r.problems.push({ text: `${r.named('behavior.judge.instructions')} is ${shownValue(said)}, where text is needed` });
1157
1179
  ok = false;
1158
1180
  } else if (said.length > MAX_JUDGE_INSTRUCTIONS) {
1159
1181
  r.problems.push({
1160
- text: `behavior.judge.instructions is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
1182
+ text: `${r.named('behavior.judge.instructions')} is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
1161
1183
  });
1162
1184
  ok = false;
1163
1185
  } else {
@@ -1184,7 +1206,7 @@ function readKeyBy(r: Reader, kind: 'rate_limit' | 'passive_sample', value: unkn
1184
1206
  if (value === undefined) return undefined;
1185
1207
  if (typeof value === 'string') return value;
1186
1208
  r.problems.push({
1187
- text: `behavior.${kind}.keyBy is ${shownValue(value)}, where a field name is needed, such as "channelId"`,
1209
+ text: `${r.named(`behavior.${kind}.keyBy`)} is ${shownValue(value)}, where a field name is needed, such as "channelId"`,
1188
1210
  read: `so it does nothing, and every event the rule matches shares one ${kind === 'rate_limit' ? 'bucket' : 'count'}`,
1189
1211
  });
1190
1212
  return undefined;
@@ -1299,7 +1321,10 @@ function readMatch(r: Reader, value: unknown): GatePolicyMatch {
1299
1321
  new RegExp(f.pattern);
1300
1322
  match.filter = { type, pattern: f.pattern };
1301
1323
  } catch (e) {
1302
- r.problems.push({ text: `match.filter.pattern isn't a regular expression the gate can read (${errorText(e)})` });
1324
+ r.problems.push({
1325
+ text: `match.filter.pattern isn't a regular expression the gate can read (${quotedValue(errorText(e))}), so fix it, `
1326
+ + 'or give match.filter.type "text" to look for the pattern as plain text',
1327
+ });
1303
1328
  }
1304
1329
  }
1305
1330
  }
@@ -1341,18 +1366,17 @@ function readResets(r: Reader, value: unknown): string[] | undefined {
1341
1366
  * always, defer, skip or a debounce it would be a no-op at best and a
1342
1367
  * misleading promise at worst, so it's refused rather than ignored.
1343
1368
  */
1344
- function readPassthrough(
1345
- r: Reader, value: unknown, behavior: GateBehavior | undefined, kind: BehaviorKind | undefined,
1346
- ): boolean {
1369
+ function readPassthrough(r: Reader, value: unknown, kind: BehaviorKind | undefined): boolean {
1347
1370
  if (value === undefined || value === false) return false;
1348
1371
  if (value !== true) {
1349
1372
  r.problems.push({ text: `passthrough is ${shownValue(value)}, where true or false is needed` });
1350
1373
  return false;
1351
1374
  }
1352
1375
  if (kind === undefined || kind === 'rate_limit' || kind === 'passive_sample') return true;
1376
+ const given = kind === 'always' || kind === 'defer' || kind === 'skip' ? `behavior "${kind}"` : r.named(`behavior.${kind}`);
1353
1377
  r.problems.push({
1354
- text: `passthrough is only valid with rate_limit or passive_sample behaviors — for "${behavior ? formatBehavior(behavior) : kind}" `
1355
- + 'there is no non-firing match to fall through on',
1378
+ text: `passthrough is true, and only a ${r.named('behavior.rate_limit')} or ${r.named('behavior.passive_sample')} rule passes on `
1379
+ + `an event it matches without waking on it; with ${given}, leave passthrough out`,
1356
1380
  });
1357
1381
  return false;
1358
1382
  }
@@ -1518,7 +1542,7 @@ function readConfig(
1518
1542
  }
1519
1543
 
1520
1544
  const rules = policies.map((rule, i) => {
1521
- const reading = readRule(rule, `The ${ordinal(i + 1)} policy`, file);
1545
+ const reading = readRule(rule, `The ${ordinal(i + 1)} rule`, file);
1522
1546
  return { raw: rule, reading, outcome: ruleOutcome(reading) };
1523
1547
  });
1524
1548
  if (defaultBehavior !== 'always') {
@@ -1604,7 +1628,7 @@ function readLastRecipe(path: string): { recipe: WrittenConfig | null; problem?:
1604
1628
  if (read.refusedWhole !== undefined) return { recipe: null, problem: "it isn't read as the starting rules are" };
1605
1629
  const rules = read.rules.map((rule) => rule.raw);
1606
1630
  const twice = duplicateName(rules);
1607
- if (twice !== undefined) return { recipe: null, problem: `it names the rule "${twice}" twice` };
1631
+ if (twice !== undefined) return { recipe: null, problem: `it names the rule ${quotedValue(twice)} twice` };
1608
1632
  return { recipe: { rules, default: isPlainObject(raw) ? raw.default : undefined } };
1609
1633
  }
1610
1634
 
@@ -1614,7 +1638,7 @@ function readLastRecipe(path: string): { recipe: WrittenConfig | null; problem?:
1614
1638
  * wrote as read agrees with the same rule written more briefly.
1615
1639
  */
1616
1640
  function ruleMeaning(rule: unknown): string {
1617
- const reading = readRule(rule, 'A policy');
1641
+ const reading = readRule(rule, 'A rule');
1618
1642
  return reading.policy ? `read ${canonical(reading.policy)}` : `written ${canonical(rule)}`;
1619
1643
  }
1620
1644
 
@@ -1725,7 +1749,7 @@ function notUsedClause(write: Extract<StartWrite, { kind: 'start-without' }>, st
1725
1749
  */
1726
1750
  function outOfForce(write: Extract<StartWrite, { kind: 'start-without' }>): { said: string; fix: string; nextStart: boolean } | null {
1727
1751
  const nameless = write.nameless ?? 0;
1728
- const left = [...(write.leftOut ?? []).map((name) => `"${name}"`), ...(nameless === 0 ? [] : [nameless === 1 ? 'one with no name' : `${nameless} with no name`])];
1752
+ const left = [...(write.leftOut ?? []).map((name) => quotedValue(name)), ...(nameless === 0 ? [] : [nameless === 1 ? 'one with no name' : `${nameless} with no name`])];
1729
1753
  const held = write.heldOut ?? [];
1730
1754
  const said: string[] = [];
1731
1755
  const fix: string[] = [];
@@ -1954,14 +1978,14 @@ export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: Ev
1954
1978
  * three it is (the rule never applies; it misses some of its events; it
1955
1979
  * may not be meant for those events at all) and what to do. */
1956
1980
  export function formatShadowWarning(w: ShadowWarning): string {
1957
- const later = `"${w.later}"`;
1958
- const earlier = `"${w.earlier}"`;
1959
- const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: "${w.earlier}")`;
1981
+ const later = quotedValue(w.later);
1982
+ const earlier = quotedValue(w.earlier);
1983
+ const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: ${quotedValue(w.earlier)})`;
1960
1984
  if (w.whole) {
1961
1985
  return `${later} never applies: ${earlier} comes first and matches every event ${later} would. `
1962
1986
  + `If ${later} should apply, ${move}, or narrow ${earlier}.`;
1963
1987
  }
1964
- const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.join(', ');
1988
+ const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.map((type) => quotedValue(type).slice(1, -1)).join(', ');
1965
1989
  if (w.known) {
1966
1990
  return `${earlier} comes first and takes ${later}'s ${types} events, so ${later} applies only to its others. `
1967
1991
  + `If it should apply to those too, ${move}.`;
@@ -1994,20 +2018,23 @@ function afterChange(name: string, callback: () => void): void {
1994
2018
  }
1995
2019
 
1996
2020
  /** The privileged-users file's error when some entries of its list aren't user ids. */
1997
- function skippedPrivilegedIds(skipped: unknown[], loaded: number): string {
2021
+ function skippedPrivilegedIds(skipped: unknown[], loaded: number, path: string): string {
1998
2022
  const one = skipped.length === 1;
1999
2023
  const shown = skipped.slice(0, 5).map((x) => {
2000
2024
  if (typeof x === 'number' && Number.isInteger(x)) {
2001
2025
  return `${x} (a number this large can't be read exactly: write the id as a string, in quotes)`;
2002
2026
  }
2003
- const json = JSON.stringify(x) ?? String(x);
2027
+ const json = quotedValue(x);
2004
2028
  return json.length > 40 ? `${safeSlice(json, 0, 39)}…` : json;
2005
2029
  });
2006
2030
  const more = skipped.length > shown.length ? `, and ${skipped.length - shown.length} more` : '';
2007
- const rest = loaded === 0 ? 'no id loaded, so no one is privileged' : `${loaded} ${loaded === 1 ? 'id' : 'ids'} did load`;
2008
- return `${skipped.length} ${one ? 'entry' : 'entries'} in the privileged-users file ${one ? "isn't a user id" : "aren't user ids"}, `
2031
+ const rest = loaded === 0
2032
+ ? 'no id loaded, so no one is privileged'
2033
+ : `the ${loaded === 1 ? 'one id that did load is' : `${loaded} ids that did load are`} privileged`;
2034
+ return `${skipped.length} ${one ? 'entry' : 'entries'} in the privileged-users file, at ${path}, ${one ? "isn't a user id" : "aren't user ids"}, `
2009
2035
  + `so ${one ? 'it was' : 'they were'} skipped: ${shown.join(', ')}${more}. A user id is a non-empty string, or a safe integer `
2010
- + `(a whole number within 2^53 - 1 of zero); ${rest}.`;
2036
+ + `(a whole number within 2^53 - 1 of zero); ${rest}. Once ${one ? 'that entry is a user id' : 'those entries are user ids'}, the gate `
2037
+ + 'reads the file again at its next check for changes.';
2011
2038
  }
2012
2039
 
2013
2040
  // ---------------------------------------------------------------------------
@@ -2039,6 +2066,7 @@ export class EventGate {
2039
2066
  /** Whether gate.json's error is one that keeps what it holds out of force
2040
2067
  * (configUnreadable), so gate_status says what's in force instead. */
2041
2068
  private configUnused = false;
2069
+ private configFix: string | null = null;
2042
2070
  private privilegedError: string | null = null;
2043
2071
  /** What the start did with the host's starting rules that it couldn't use
2044
2072
  * or add (describeStart), kept apart from gate.json's own error, until the
@@ -2119,8 +2147,8 @@ export class EventGate {
2119
2147
  private sleepSuppressed = 0;
2120
2148
  private sleepTimer: ReturnType<typeof setTimeout> | null = null;
2121
2149
  private sleepAgent: string | undefined;
2122
- // From dispose() to the next resumeSleep(): no sleep or self-wake timer is
2123
- // armed, though a turn a failed stop() left running can still ask for one.
2150
+ // From dispose() on, no sleep or self-wake timer is armed, though a turn a
2151
+ // failed stop() left running can still ask for one.
2124
2152
  private disposed = false;
2125
2153
 
2126
2154
  // Self-wake timers per agent (skip_reply's wake_in_seconds). Unlike sleep,
@@ -2366,7 +2394,7 @@ export class EventGate {
2366
2394
  }
2367
2395
  const twice = duplicateName(starting.rules.map((rule) => rule.raw));
2368
2396
  if (twice !== undefined) {
2369
- this.startMergeNote = `At start, the recipe names the rule "${twice}" twice, so gate.json was seeded with it as given and it `
2397
+ this.startMergeNote = `At start, the recipe names the rule ${quotedValue(twice)} twice, so gate.json was seeded with it as given and it `
2370
2398
  + "wasn't recorded as the recipe last applied: a later start merges the recipe by name, once gate.json and the recipe "
2371
2399
  + 'name each rule once.';
2372
2400
  } else {
@@ -2560,7 +2588,13 @@ export class EventGate {
2560
2588
  try {
2561
2589
  raw = readFileSync(this.configPath, 'utf-8');
2562
2590
  } catch (err) {
2563
- if (!isMissing(err)) return this.configUnreadable(err);
2591
+ if (!isMissing(err)) {
2592
+ const why = quotedValue(errorText(err));
2593
+ return this.configUnreadable(
2594
+ why, `gate.json, at ${this.configPath}, can't be read: ${why}`,
2595
+ 'Whoever runs this host can make it a file the gate can read, and the gate reads it again at its next check for changes.',
2596
+ );
2597
+ }
2564
2598
  // No file is no problem with it: the rules in force stay.
2565
2599
  this.configError = null;
2566
2600
  this.configUnused = false;
@@ -2579,12 +2613,25 @@ export class EventGate {
2579
2613
  try {
2580
2614
  parsed = parseGateJson(raw);
2581
2615
  } catch (err) {
2582
- return { ...this.configUnreadable(err), notJson: true };
2616
+ const why = quotedValue(errorText(err));
2617
+ return {
2618
+ ...this.configUnreadable(
2619
+ why, `gate.json, at ${this.configPath}, isn't JSON: ${why}`,
2620
+ "Once it's JSON, the gate reads it again at its next check for changes.",
2621
+ ),
2622
+ notJson: true,
2623
+ };
2583
2624
  }
2584
2625
  const read = readConfig(parsed, 'gate.json');
2585
2626
  // JSON the gate doesn't use is still the operator's file, edited as written.
2586
2627
  if ('refused' in read) {
2587
- return { ...this.configUnreadable(new Error(read.refused)), state: 'refused', listed: read.listed, silenced: read.silenced };
2628
+ const problem = read.refused.startsWith('gate.json ')
2629
+ ? `gate.json, at ${this.configPath},${read.refused.slice('gate.json'.length)}`
2630
+ : read.refused;
2631
+ return {
2632
+ ...this.configUnreadable(read.refused, problem, 'Once gate.json is mended, the gate reads it again at its next check for changes.'),
2633
+ state: 'refused', listed: read.listed, silenced: read.silenced,
2634
+ };
2588
2635
  }
2589
2636
  // A field the gate doesn't read does nothing, and a rule is read past a
2590
2637
  // problem or left out, so the file loads, and each is named.
@@ -2598,20 +2645,20 @@ export class EventGate {
2598
2645
  * gate.json's current error, replacing the last one, and traced and logged
2599
2646
  * when it's new, so an edit that doesn't load is in the log once.
2600
2647
  */
2601
- private configUnreadable(err: unknown): { state: 'unreadable'; error: string } {
2602
- const msg = err instanceof Error ? err.message : String(err);
2603
- if (msg !== this.configError) {
2604
- console.error(`[gate] ${this.configPath} couldn't be used (${msg}), so what it holds isn't in force`);
2648
+ private configUnreadable(why: string, problem: string, fix: string | null = null): { state: 'unreadable'; error: string } {
2649
+ if (problem !== this.configError) {
2650
+ console.error(`[gate] ${this.configPath} couldn't be used (${why}), so what it holds isn't in force${fix ? `. ${fix}` : ''}`);
2605
2651
  this.emitTrace({
2606
2652
  type: 'gate:config-error',
2607
- error: msg,
2653
+ error: why,
2608
2654
  configPath: this.configPath,
2609
2655
  timestamp: Date.now(),
2610
2656
  });
2611
2657
  }
2612
- this.configError = msg;
2658
+ this.configError = problem;
2659
+ this.configFix = fix;
2613
2660
  this.configUnused = true;
2614
- return { state: 'unreadable', error: msg };
2661
+ return { state: 'unreadable', error: why };
2615
2662
  }
2616
2663
 
2617
2664
  /**
@@ -2620,7 +2667,8 @@ export class EventGate {
2620
2667
  */
2621
2668
  private configErrorShown(): string | null {
2622
2669
  if (this.configError === null || !this.configUnused) return this.configError;
2623
- return `${sentence(this.configError)} So what gate.json holds isn't in force, and the gate runs ${this.rulesRun()}.`;
2670
+ return `${sentence(this.configError)} So what gate.json holds isn't in force, and the gate runs ${this.rulesRun()}.`
2671
+ + (this.configFix ? ` ${this.configFix}` : '');
2624
2672
  }
2625
2673
 
2626
2674
  /** What the gate runs, by where its rules came from, for a text that says so: "the host's starting rules". */
@@ -2665,13 +2713,13 @@ export class EventGate {
2665
2713
  | { top: Record<string, unknown>; rules: unknown[]; remedy?: string }
2666
2714
  | { refused: string } {
2667
2715
  const read = this.readConfigFile();
2668
- if (read.state === 'unreadable') return { refused: unusableRefusal(read, this.rulesRun()) };
2716
+ if (read.state === 'unreadable') return { refused: unusableRefusal(read, this.rulesRun(), this.configPath) };
2669
2717
  if (read.state === 'loaded' || read.state === 'refused') {
2670
- if ('noList' in read.listed) return { refused: noListRefusal(read.listed.noList, this.rulesRun()) };
2718
+ if ('noList' in read.listed) return { refused: noListRefusal(read.listed.noList, this.rulesRun(), this.configPath) };
2671
2719
  // Rules not gate.json's in force while it holds the text in force: what a
2672
2720
  // start ran in place of one with no rules of its own, until it changes.
2673
2721
  if (read.state === 'loaded' && read.text === this.configText && this.configSource !== 'file') {
2674
- return { refused: notUsedRefusal(this.rulesRun()) };
2722
+ return { refused: notUsedRefusal(this.rulesRun(), this.configPath) };
2675
2723
  }
2676
2724
  const edit = { top: read.listed.top, rules: [...read.listed.rules] };
2677
2725
  if (read.state === 'loaded') return edit;
@@ -2688,9 +2736,9 @@ export class EventGate {
2688
2736
  return {
2689
2737
  ...edit,
2690
2738
  remedy: sharing === 1
2691
- ? `${removes ? 'Replacing or removing' : 'Replacing'} "${only}" by name would bring gate.json into force.`
2739
+ ? `${removes ? 'Replacing or removing' : 'Replacing'} ${quotedValue(only)} by name would bring gate.json into force.`
2692
2740
  : sharing > 1
2693
- ? `gate.json has ${sharing} rules named "${only}", and a change by that name replaces the first of them`
2741
+ ? `gate.json has ${sharing} rules named ${quotedValue(only)}, and a change by that name replaces the first of them`
2694
2742
  + `${removes ? ' or removes them all' : ''}.`
2695
2743
  : 'No change through the gate can bring it into force, so gate.json has to be fixed by an edit of its own; '
2696
2744
  + 'until then, a change is refused the same way.',
@@ -2864,9 +2912,18 @@ export class EventGate {
2864
2912
  options?: {
2865
2913
  position?: 'append' | 'prepend' | { before: string } | { after: string };
2866
2914
  onIgnoredFields?: (notes: string[]) => void;
2915
+ /** The caller's own name for a field a refusal names, keyed by gate.json's name for it: one of
2916
+ * `behavior.debounce`, `behavior.rate_limit`, `behavior.passive_sample` and `behavior.judge`, or
2917
+ * `position.before` and `position.after` for a placement. A key names the fields under it too:
2918
+ * `behavior.rate_limit` gives `behavior.rate_limit.tokens` its name, and a longer key wins.
2919
+ * Other fields keep gate.json's names.
2920
+ *
2921
+ * @internal For `wake_add_rule`'s refusals; not a host API. */
2922
+ fieldNames?: Readonly<Record<string, string>>;
2867
2923
  },
2868
2924
  ): GatePolicy {
2869
- const reading = readRule(rawPolicy, 'A policy');
2925
+ const named = namedBy(options?.fieldNames);
2926
+ const reading = readRule(rawPolicy, 'A rule', 'gate.json', named);
2870
2927
  if (reading.problems.length > 0 || !reading.policy) throw new Error(refusalOf(reading));
2871
2928
  const policy = reading.policy;
2872
2929
  const ignored = notesOf(reading);
@@ -2886,9 +2943,26 @@ export class EventGate {
2886
2943
  const anchorName = 'before' in position ? position.before : position.after;
2887
2944
  const anchorIdx = rules.findIndex((rule) => nameOf(rule) === anchorName);
2888
2945
  if (anchorIdx < 0) {
2946
+ const field = named('before' in position ? 'position.before' : 'position.after');
2947
+ const others = [...new Set(rules.map(nameOf).filter((name): name is string => name !== undefined))];
2948
+ if (idx >= 0 && anchorName === policy.name) {
2949
+ throw new Error(
2950
+ `${reading.where} wasn't saved: ${field} is ${quotedValue(anchorName)}, the rule being saved, and a rule can't be `
2951
+ + 'placed beside itself. '
2952
+ + (others.length > 0
2953
+ ? `The other rules are ${quotedList(others)}. Give ${field} one of those to place the rule beside it, or leave it out`
2954
+ : `There are no other rules, so leave ${field} out`)
2955
+ + ' to replace the rule where it is.'
2956
+ + savedOnlyInForce(this.configPath, remedy),
2957
+ );
2958
+ }
2889
2959
  throw new Error(
2890
- `addPolicy: no policy named "${anchorName}" to anchor on — `
2891
- + `have: [${rules.map(nameOf).filter((name) => name !== undefined).join(', ')}]`,
2960
+ `${reading.where} wasn't saved: ${field} is ${quotedValue(anchorName)}, and no rule has that name. `
2961
+ + (others.length > 0
2962
+ ? `The other rules are ${quotedList(others)}. Give ${field} one of those to place the rule beside it, or leave it out`
2963
+ : `There are no other rules, so leave ${field} out`)
2964
+ + ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
2965
+ + savedOnlyInForce(this.configPath, remedy),
2892
2966
  );
2893
2967
  }
2894
2968
  rules.splice('before' in position ? anchorIdx : anchorIdx + 1, 0, policy);
@@ -2963,6 +3037,16 @@ export class EventGate {
2963
3037
  return current.policies.map((p) => p.name);
2964
3038
  }
2965
3039
 
3040
+ /** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
3041
+ ruleNamesForChange(): { names: string[]; note: string } | { refused: string } {
3042
+ const edit = this.rulesForChange();
3043
+ if ('refused' in edit) return { refused: edit.refused };
3044
+ return {
3045
+ names: [...new Set(edit.rules.map(nameOf).filter((name): name is string => name !== undefined))],
3046
+ note: savedOnlyInForce(this.configPath, edit.remedy),
3047
+ };
3048
+ }
3049
+
2966
3050
  /**
2967
3051
  * Persist gate.json and apply it in memory together: `top`, its own fields,
2968
3052
  * with `rules` as its rule list. Only what loads is written: a change to a
@@ -2989,15 +3073,16 @@ export class EventGate {
2989
3073
  const read = readConfig(JSON.parse(text), 'gate.json');
2990
3074
  if ('refused' in read) {
2991
3075
  throw new Error(
2992
- `Nothing was saved: with this change, gate.json wouldn't be used. ${sentence(read.refused)}${remedy ? ` ${remedy}` : ''}`,
3076
+ `Nothing was saved: with this change, gate.json, at ${this.configPath}, wouldn't be used. ${sentence(read.refused)}`
3077
+ + (remedy ? ` ${remedy}` : ''),
2993
3078
  );
2994
3079
  }
2995
3080
  // One the gate doesn't use, and that a change can mend, holds back what a
2996
3081
  // rule it leaves out would wake on, so its default holds events back.
2997
3082
  if (remedy !== undefined && read.config.policies.length === 0) {
2998
3083
  throw new Error(
2999
- 'Nothing was saved: with this change, gate.json would have no rules the gate reads, and its default would hold back every '
3000
- + `event. Meanwhile the gate runs ${this.rulesRun()}. ${remedy}`,
3084
+ `Nothing was saved: with this change, gate.json, at ${this.configPath}, would have no rules the gate reads, and its default `
3085
+ + `would hold back every event. Meanwhile the gate runs ${this.rulesRun()}. ${remedy}`,
3001
3086
  );
3002
3087
  }
3003
3088
  try {
@@ -3679,7 +3764,7 @@ export class EventGate {
3679
3764
  } catch (err) {
3680
3765
  const one = events.length === 1;
3681
3766
  console.error(
3682
- `[gate] Policy "${policyName}": its pending batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
3767
+ `[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
3683
3768
  + `(${errorText(err)}), so the batch is dropped`,
3684
3769
  );
3685
3770
  return;
@@ -3877,7 +3962,7 @@ export class EventGate {
3877
3962
  ).finally(() => {
3878
3963
  if (state.timer) clearTimeout(state.timer);
3879
3964
  }).catch((err: unknown) => {
3880
- console.error(`[gate] Policy "${policyName}": the judge's verdict couldn't be applied (${errorText(err)})`);
3965
+ console.error(`[gate] Rule ${quotedValue(policyName)}: the judge's verdict couldn't be applied (${errorText(err)})`);
3881
3966
  });
3882
3967
  }
3883
3968
 
@@ -3948,8 +4033,8 @@ export class EventGate {
3948
4033
  const n = state.events.length;
3949
4034
  const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
3950
4035
  wakes.push(state.judge
3951
- ? { at: state.dueAt, certain: false, what: `rule "${name}"'s batch of ${batch} goes to the judge` }
3952
- : { at: state.dueAt, certain: true, what: `rule "${name}"'s debounced batch of ${batch}` });
4036
+ ? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
4037
+ : { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
3953
4038
  }
3954
4039
  return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
3955
4040
  }
@@ -3999,7 +4084,7 @@ export class EventGate {
3999
4084
  if (failed && (err as { providerAdmission?: unknown } | undefined)?.providerAdmission === 'deferred') {
4000
4085
  const wakes = this.judgeOnError === 'wake';
4001
4086
  console.error(
4002
- `[gate] Policy "${policyName}": the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
4087
+ `[gate] Rule ${quotedValue(policyName)}: the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
4003
4088
  + `${state.events.length === 1 ? 'event' : 'events'} ${wakes ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
4004
4089
  );
4005
4090
  if (this.disposed) return;
@@ -4031,7 +4116,7 @@ export class EventGate {
4031
4116
  const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
4032
4117
  if (failed) {
4033
4118
  console.error(
4034
- `[gate] Policy "${policyName}": the reply judge failed (${reason}), so its batch of ${state.events.length} `
4119
+ `[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
4035
4120
  + `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`,
4036
4121
  );
4037
4122
  }
@@ -4125,11 +4210,10 @@ export class EventGate {
4125
4210
 
4126
4211
  /** A turn that continues another (a budget restart, a tool-result-guard
4127
4212
  * retry) holds the entry its predecessor kept for it, and takes it again
4128
- * where that entry ended in between: at a stop(), whose dispose() clears
4129
- * every entry, or at a provider hold that kept the turn from starting,
4130
- * whose end queues it again. Unlike onInferenceStarted it leaves a
4131
- * pending self-wake alone: the logical turn that armed it is the one
4132
- * still running. */
4213
+ * where that entry ended in between, as at a provider hold that kept the
4214
+ * turn from starting, whose end queues it again. Unlike
4215
+ * onInferenceStarted it leaves a pending self-wake alone: the logical
4216
+ * turn that armed it is the one still running. */
4133
4217
  onInferenceResumed(agentName: string): void {
4134
4218
  this.inferring.add(agentName);
4135
4219
  if (this.changesJudgedContext(agentName)) this.stopJudging();
@@ -4254,7 +4338,7 @@ export class EventGate {
4254
4338
  // Arm the self-wake: when the window elapses, clear sleep and request an
4255
4339
  // inference so the agent resumes on its own (not dependent on a heartbeat
4256
4340
  // tick or an incoming message). Cancelled by clearSleep()/`wake`, and by
4257
- // dispose(), after which start() arms it again (resumeSleep).
4341
+ // dispose(), after which nothing arms it.
4258
4342
  this.armSleepWake();
4259
4343
  this.reloadPrivilegedIfChanged();
4260
4344
  this.emitTrace({
@@ -4268,24 +4352,11 @@ export class EventGate {
4268
4352
  return { until: this.sleepUntil };
4269
4353
  }
4270
4354
 
4271
- /**
4272
- * Arm timers again for a lifecycle that starts after a stop(), and the
4273
- * sleep's wake first: dispose() clears its timer and keeps the sleep. A
4274
- * sleep still ahead wakes the agent at its end; one that ended while
4275
- * stopped wakes it now, since that wake is what the agent asked for. With
4276
- * no sleep, wakeFromSleep does nothing.
4277
- */
4278
- resumeSleep(): void {
4279
- this.disposed = false;
4280
- if (this.sleepUntil > this.now()) this.armSleepWake();
4281
- else this.wakeFromSleep('sleep-expired');
4282
- }
4283
-
4284
4355
  /**
4285
4356
  * Arm the sleep's wake for its end by the gate's own clock. One timer holds
4286
4357
  * at most MAX_TIMER_MS, about 24.8 days, so a longer sleep waits in steps,
4287
4358
  * and wakeFromSleep reads the clock as each one fires. Nothing is armed
4288
- * while the gate is disposed: resumeSleep() arms it at the next start().
4359
+ * once the gate is disposed.
4289
4360
  */
4290
4361
  private armSleepWake(): void {
4291
4362
  if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
@@ -4394,9 +4465,7 @@ export class EventGate {
4394
4465
  * fires early (Node's can, by a millisecond), or a clock set back.
4395
4466
  */
4396
4467
  private wakeFromSleep(reason: string): void {
4397
- // The timer that called it has fired; from resumeSleep(), for a sleep
4398
- // already over, one may still be armed, and it goes, so no stale step of
4399
- // it can fire later.
4468
+ // The timer that called it has fired.
4400
4469
  if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
4401
4470
  if (this.sleepUntil === 0) return;
4402
4471
  if (this.sleepUntil > this.now()) {
@@ -4453,14 +4522,20 @@ export class EventGate {
4453
4522
  * each a non-empty string or a safe integer (a whole number within 2^53 - 1 of zero). An absent file
4454
4523
  * means no one is privileged. A file that can't be read, isn't JSON, or
4455
4524
  * holds no list leaves the list that last loaded in this process in effect
4456
- * (none, at startup), with its error in gate_status until the file is read
4457
- * again. In a list, an entry that isn't a user id is skipped, and named in
4458
- * the file's error, and the rest load. With `unlessUnchanged`, a file whose
4525
+ * (none, at startup), with its error in gate_status until a later read
4526
+ * loads it, finds it gone, or fails another way. In a list, an entry that isn't a user id is skipped, and named
4527
+ * in the file's error, and the rest load. With `unlessUnchanged`, a file whose
4459
4528
  * text is the text last read isn't parsed again.
4460
4529
  */
4461
4530
  private loadPrivileged(opts?: { unlessUnchanged?: boolean }): void {
4462
4531
  if (!this.privilegedUsersPath) return;
4463
- const failed = (why: string): string => `Failed to load privileged-users file: ${why}`;
4532
+ const file = `The privileged-users file, at ${this.privilegedUsersPath},`;
4533
+ const held = this.privilegedUserIds.size;
4534
+ const meanwhile = held === 0
4535
+ ? 'no one is privileged'
4536
+ : `${held === 1 ? 'the one id of the list that last loaded stays' : `the ${held} ids of the list that last loaded stay`} privileged`;
4537
+ const revoke = 'Replacing it with a file holding [], or removing it, makes no one privileged at the gate\'s next check for changes.';
4538
+ const failed = (problem: string, fix: string): string => `${file} ${problem}, so ${meanwhile}. ${fix} ${revoke}`;
4464
4539
  let raw: string;
4465
4540
  try {
4466
4541
  raw = readFileSync(this.privilegedUsersPath, 'utf8');
@@ -4472,24 +4547,28 @@ export class EventGate {
4472
4547
  this.privilegedUserIds = new Set();
4473
4548
  this.privilegedError = null;
4474
4549
  } else {
4475
- this.privilegedError = failed(err instanceof Error ? err.message : String(err));
4550
+ this.privilegedError = failed(
4551
+ `can't be read: ${quotedValue(errorText(err))}`,
4552
+ 'Whoever runs this host can make it a file the gate can read, and the gate reads it again at its next check for changes.',
4553
+ );
4476
4554
  }
4477
4555
  return;
4478
4556
  }
4479
4557
  if (opts?.unlessUnchanged && raw === this.privilegedText) return;
4480
4558
  this.privilegedText = raw;
4559
+ const fixed = 'Once it holds a list of user ids, the gate loads it at its next check for changes.';
4481
4560
  let ids: unknown;
4482
4561
  try {
4483
4562
  const parsed: unknown = parseGateJson(raw);
4484
4563
  ids = Array.isArray(parsed) ? parsed : (parsed as { userIds?: unknown } | null)?.userIds;
4485
4564
  } catch (err) {
4486
- this.privilegedError = failed(err instanceof Error ? err.message : String(err));
4565
+ this.privilegedError = failed(`isn't JSON: ${quotedValue(errorText(err))}`, fixed);
4487
4566
  return;
4488
4567
  }
4489
4568
  // A file that parses but holds no list hasn't loaded either: it used to
4490
4569
  // mean no one is privileged, without a word.
4491
4570
  if (!Array.isArray(ids)) {
4492
- this.privilegedError = failed('expected a list of user ids, or {"userIds": [...]}');
4571
+ this.privilegedError = failed('holds no list of user ids, where one is needed, such as ["123"] or {"userIds": ["123"]}', fixed);
4493
4572
  return;
4494
4573
  }
4495
4574
  const loaded = new Set<string>();
@@ -4502,7 +4581,7 @@ export class EventGate {
4502
4581
  else skipped.push(id);
4503
4582
  }
4504
4583
  this.privilegedUserIds = loaded;
4505
- this.privilegedError = skipped.length > 0 ? skippedPrivilegedIds(skipped, loaded.size) : null;
4584
+ this.privilegedError = skipped.length > 0 ? skippedPrivilegedIds(skipped, loaded.size, this.privilegedUsersPath) : null;
4506
4585
  }
4507
4586
 
4508
4587
  /** At most once a second, the privileged-users file read again, and loaded when its text has changed. */
@@ -4660,9 +4739,9 @@ export class EventGate {
4660
4739
  this.selfWakeDue.clear();
4661
4740
  // The sleep stays, with its note and agent, and its timer goes: fired
4662
4741
  // after a stop(), its wake reached a framework that had stopped, and the
4663
- // timer held the process until the sleep's end. The next start() arms it
4664
- // again (resumeSleep). Until then nothing arms one, nor a self-wake: a
4665
- // turn a failed stop() left running can still call sleep or skip_reply.
4742
+ // timer held the process until the sleep's end. Nothing arms one after
4743
+ // this, nor a self-wake: a turn a failed stop() left running can still
4744
+ // call sleep or skip_reply.
4666
4745
  if (this.sleepTimer) { clearTimeout(this.sleepTimer); this.sleepTimer = null; }
4667
4746
  this.disposed = true;
4668
4747
  this.rateLimitBuckets.clear();
@@ -4672,13 +4751,9 @@ export class EventGate {
4672
4751
  this.inferenceBuffer = [];
4673
4752
  // stop() disposes the gate once its streams and event loop have ended (a
4674
4753
  // turn still compiling then is dropped at its next check, before it sends
4675
- // anything), and a start() goes on with this gate. An agent left here by
4676
- // a turn whose exit didn't end it (a retry stop() cancelled, a successor
4677
- // stop() refused) would make every debounced batch buffer until that
4678
- // agent's next turn ended. A turn that continues after the start() takes
4679
- // its entry again (onInferenceResumed). A start() made during the stop()
4680
- // is refused, so no turn of a new lifecycle can have begun before this
4681
- // runs.
4754
+ // anything). An agent left here by a turn whose exit didn't end it (a
4755
+ // retry stop() cancelled, a successor stop() refused) would make every
4756
+ // debounced batch buffer until that agent's next turn ended.
4682
4757
  this.inferring.clear();
4683
4758
  }
4684
4759
  }