@animalabs/agent-framework 0.29.0 → 0.31.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 (236) hide show
  1. package/README.md +71 -8
  2. package/dist/src/admission/admit.d.ts +90 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -0
  4. package/dist/src/admission/admit.js +43 -0
  5. package/dist/src/admission/admit.js.map +1 -0
  6. package/dist/src/agent.d.ts +50 -9
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +125 -23
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +10 -3
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/author-presentation.d.ts +50 -0
  14. package/dist/src/author-presentation.d.ts.map +1 -0
  15. package/dist/src/author-presentation.js +58 -0
  16. package/dist/src/author-presentation.js.map +1 -0
  17. package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/render.js +7 -6
  19. package/dist/src/code-execution/isolate/render.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +5 -4
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/dendrite/index.d.ts +6 -0
  24. package/dist/src/dendrite/index.d.ts.map +1 -0
  25. package/dist/src/dendrite/index.js +3 -0
  26. package/dist/src/dendrite/index.js.map +1 -0
  27. package/dist/src/dendrite/presets.d.ts +60 -0
  28. package/dist/src/dendrite/presets.d.ts.map +1 -0
  29. package/dist/src/dendrite/presets.js +117 -0
  30. package/dist/src/dendrite/presets.js.map +1 -0
  31. package/dist/src/dendrite/registry.d.ts +260 -0
  32. package/dist/src/dendrite/registry.d.ts.map +1 -0
  33. package/dist/src/dendrite/registry.js +742 -0
  34. package/dist/src/dendrite/registry.js.map +1 -0
  35. package/dist/src/dendrite/types.d.ts +392 -0
  36. package/dist/src/dendrite/types.d.ts.map +1 -0
  37. package/dist/src/dendrite/types.js +24 -0
  38. package/dist/src/dendrite/types.js.map +1 -0
  39. package/dist/src/framework.d.ts +806 -118
  40. package/dist/src/framework.d.ts.map +1 -1
  41. package/dist/src/framework.js +4180 -1121
  42. package/dist/src/framework.js.map +1 -1
  43. package/dist/src/gate/event-gate.d.ts +36 -21
  44. package/dist/src/gate/event-gate.d.ts.map +1 -1
  45. package/dist/src/gate/event-gate.js +223 -149
  46. package/dist/src/gate/event-gate.js.map +1 -1
  47. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
  48. package/dist/src/gate/recipe-reconcile.js +6 -5
  49. package/dist/src/gate/recipe-reconcile.js.map +1 -1
  50. package/dist/src/host-presentation.d.ts +4 -0
  51. package/dist/src/host-presentation.d.ts.map +1 -1
  52. package/dist/src/host-presentation.js +12 -10
  53. package/dist/src/host-presentation.js.map +1 -1
  54. package/dist/src/index.d.ts +6 -2
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/index.js +7 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/inference-bounds.d.ts +37 -0
  59. package/dist/src/inference-bounds.d.ts.map +1 -0
  60. package/dist/src/inference-bounds.js +84 -0
  61. package/dist/src/inference-bounds.js.map +1 -0
  62. package/dist/src/line-reader.d.ts +7 -1
  63. package/dist/src/line-reader.d.ts.map +1 -1
  64. package/dist/src/line-reader.js +17 -6
  65. package/dist/src/line-reader.js.map +1 -1
  66. package/dist/src/mcpl/channel-registry.d.ts +84 -9
  67. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  68. package/dist/src/mcpl/channel-registry.js +136 -58
  69. package/dist/src/mcpl/channel-registry.js.map +1 -1
  70. package/dist/src/mcpl/errors.d.ts +1 -0
  71. package/dist/src/mcpl/errors.d.ts.map +1 -1
  72. package/dist/src/mcpl/errors.js +3 -0
  73. package/dist/src/mcpl/errors.js.map +1 -1
  74. package/dist/src/mcpl/inbound-source.d.ts +2 -2
  75. package/dist/src/mcpl/inbound-source.js +2 -2
  76. package/dist/src/mcpl/inference-router.d.ts +11 -1
  77. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  78. package/dist/src/mcpl/inference-router.js +65 -9
  79. package/dist/src/mcpl/inference-router.js.map +1 -1
  80. package/dist/src/mcpl/modern-connection.d.ts +10 -7
  81. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  82. package/dist/src/mcpl/modern-connection.js +85 -74
  83. package/dist/src/mcpl/modern-connection.js.map +1 -1
  84. package/dist/src/mcpl/protocol-family.d.ts +1 -1
  85. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  86. package/dist/src/mcpl/protocol-family.js +45 -3
  87. package/dist/src/mcpl/protocol-family.js.map +1 -1
  88. package/dist/src/mcpl/push-coalescer.d.ts +11 -7
  89. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  90. package/dist/src/mcpl/push-coalescer.js +21 -0
  91. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  92. package/dist/src/mcpl/references.d.ts +1 -2
  93. package/dist/src/mcpl/references.d.ts.map +1 -1
  94. package/dist/src/mcpl/references.js +3 -3
  95. package/dist/src/mcpl/references.js.map +1 -1
  96. package/dist/src/mcpl/server-connection.d.ts +14 -0
  97. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  98. package/dist/src/mcpl/server-connection.js +26 -22
  99. package/dist/src/mcpl/server-connection.js.map +1 -1
  100. package/dist/src/mcpl/tool-signature.d.ts +1 -0
  101. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  102. package/dist/src/mcpl/tool-signature.js +16 -11
  103. package/dist/src/mcpl/tool-signature.js.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  105. package/dist/src/mcpl/toolsets-module.js +7 -0
  106. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  107. package/dist/src/mcpl/transport.d.ts +44 -21
  108. package/dist/src/mcpl/transport.d.ts.map +1 -1
  109. package/dist/src/mcpl/transport.js +283 -64
  110. package/dist/src/mcpl/transport.js.map +1 -1
  111. package/dist/src/mcpl/types.d.ts +17 -1
  112. package/dist/src/mcpl/types.d.ts.map +1 -1
  113. package/dist/src/mcpl/types.js.map +1 -1
  114. package/dist/src/module-registry.d.ts +32 -4
  115. package/dist/src/module-registry.d.ts.map +1 -1
  116. package/dist/src/module-registry.js +189 -25
  117. package/dist/src/module-registry.js.map +1 -1
  118. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
  119. package/dist/src/modules/discord/discord-js-client.js +24 -16
  120. package/dist/src/modules/discord/discord-js-client.js.map +1 -1
  121. package/dist/src/modules/sender-name.d.ts +5 -4
  122. package/dist/src/modules/sender-name.d.ts.map +1 -1
  123. package/dist/src/modules/sender-name.js +7 -5
  124. package/dist/src/modules/sender-name.js.map +1 -1
  125. package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
  126. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  127. package/dist/src/modules/workspace/disk-agreement.js +33 -12
  128. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  129. package/dist/src/modules/workspace/index.d.ts +6 -3
  130. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/index.js +61 -15
  132. package/dist/src/modules/workspace/index.js.map +1 -1
  133. package/dist/src/modules/workspace/observe.d.ts +9 -1
  134. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  135. package/dist/src/modules/workspace/observe.js +50 -10
  136. package/dist/src/modules/workspace/observe.js.map +1 -1
  137. package/dist/src/modules/workspace/reconcile.d.ts +11 -1
  138. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  139. package/dist/src/modules/workspace/reconcile.js +19 -8
  140. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  141. package/dist/src/modules/workspace/types.d.ts +8 -0
  142. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  143. package/dist/src/modules/workspace/types.js.map +1 -1
  144. package/dist/src/modules/workspace/watcher.d.ts +7 -0
  145. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/watcher.js +7 -5
  147. package/dist/src/modules/workspace/watcher.js.map +1 -1
  148. package/dist/src/notice-tags.d.ts +2 -0
  149. package/dist/src/notice-tags.d.ts.map +1 -1
  150. package/dist/src/notice-tags.js +2 -0
  151. package/dist/src/notice-tags.js.map +1 -1
  152. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  153. package/dist/src/recovery/discord-awareness-outbox.js +1 -9
  154. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  155. package/dist/src/silent-heartbeat.d.ts +4 -2
  156. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  157. package/dist/src/silent-heartbeat.js +7 -4
  158. package/dist/src/silent-heartbeat.js.map +1 -1
  159. package/dist/src/stored-injections.d.ts +4 -0
  160. package/dist/src/stored-injections.d.ts.map +1 -1
  161. package/dist/src/stored-injections.js +6 -0
  162. package/dist/src/stored-injections.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +2 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -1
  165. package/dist/src/supplied-text.js +16 -7
  166. package/dist/src/supplied-text.js.map +1 -1
  167. package/dist/src/tool-result-guard.js +1 -1
  168. package/dist/src/tool-result-guard.js.map +1 -1
  169. package/dist/src/tune-out/coordinator.d.ts +368 -16
  170. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  171. package/dist/src/tune-out/coordinator.js +1224 -136
  172. package/dist/src/tune-out/coordinator.js.map +1 -1
  173. package/dist/src/tune-out/tools.d.ts +115 -7
  174. package/dist/src/tune-out/tools.d.ts.map +1 -1
  175. package/dist/src/tune-out/tools.js +55 -18
  176. package/dist/src/tune-out/tools.js.map +1 -1
  177. package/dist/src/types/agent.d.ts +27 -1
  178. package/dist/src/types/agent.d.ts.map +1 -1
  179. package/dist/src/types/framework.d.ts +153 -15
  180. package/dist/src/types/framework.d.ts.map +1 -1
  181. package/dist/src/types/module.d.ts +42 -3
  182. package/dist/src/types/module.d.ts.map +1 -1
  183. package/dist/src/types/trace.d.ts +140 -3
  184. package/dist/src/types/trace.d.ts.map +1 -1
  185. package/package.json +4 -4
  186. package/src/admission/admit.ts +113 -0
  187. package/src/agent.ts +138 -31
  188. package/src/api/mcp-server-core.ts +10 -3
  189. package/src/author-presentation.ts +81 -0
  190. package/src/code-execution/isolate/render.ts +7 -6
  191. package/src/code-execution/isolate/scripts.ts +5 -4
  192. package/src/dendrite/index.ts +32 -0
  193. package/src/dendrite/presets.ts +149 -0
  194. package/src/dendrite/registry.ts +808 -0
  195. package/src/dendrite/types.ts +397 -0
  196. package/src/framework.ts +4344 -1052
  197. package/src/gate/event-gate.ts +251 -150
  198. package/src/gate/recipe-reconcile.ts +7 -5
  199. package/src/host-presentation.ts +14 -10
  200. package/src/index.ts +16 -2
  201. package/src/inference-bounds.ts +111 -0
  202. package/src/line-reader.ts +22 -5
  203. package/src/mcpl/channel-registry.ts +170 -60
  204. package/src/mcpl/errors.ts +4 -0
  205. package/src/mcpl/inbound-source.ts +2 -2
  206. package/src/mcpl/inference-router.ts +67 -10
  207. package/src/mcpl/modern-connection.ts +83 -68
  208. package/src/mcpl/protocol-family.ts +37 -4
  209. package/src/mcpl/push-coalescer.ts +21 -7
  210. package/src/mcpl/references.ts +3 -4
  211. package/src/mcpl/server-connection.ts +30 -21
  212. package/src/mcpl/tool-signature.ts +18 -11
  213. package/src/mcpl/toolsets-module.ts +7 -0
  214. package/src/mcpl/transport.ts +261 -60
  215. package/src/mcpl/types.ts +18 -1
  216. package/src/module-registry.ts +178 -25
  217. package/src/modules/discord/discord-js-client.ts +33 -29
  218. package/src/modules/sender-name.ts +8 -5
  219. package/src/modules/workspace/disk-agreement.ts +39 -15
  220. package/src/modules/workspace/index.ts +65 -14
  221. package/src/modules/workspace/observe.ts +51 -10
  222. package/src/modules/workspace/reconcile.ts +22 -8
  223. package/src/modules/workspace/types.ts +5 -0
  224. package/src/modules/workspace/watcher.ts +8 -6
  225. package/src/notice-tags.ts +2 -0
  226. package/src/recovery/discord-awareness-outbox.ts +1 -11
  227. package/src/silent-heartbeat.ts +7 -4
  228. package/src/stored-injections.ts +7 -0
  229. package/src/supplied-text.ts +17 -6
  230. package/src/tool-result-guard.ts +1 -1
  231. package/src/tune-out/coordinator.ts +1374 -141
  232. package/src/tune-out/tools.ts +166 -13
  233. package/src/types/agent.ts +30 -1
  234. package/src/types/framework.ts +157 -15
  235. package/src/types/module.ts +42 -3
  236. package/src/types/trace.ts +154 -3
@@ -11,7 +11,7 @@
11
11
  * inference tokens, not whether the agent should know about the event.
12
12
  */
13
13
  import { NOTICE_TAG } from '../notice-tags.js';
14
- import { cutToFit, suppliedValue } from '../supplied-text.js';
14
+ import { cutToFit, quotedJson, quotedValue, suppliedValue } from '../supplied-text.js';
15
15
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
16
  import { basename, dirname, join } from 'node:path';
17
17
  import { GateScript } from './gate-script.js';
@@ -284,10 +284,10 @@ function beforeWriting(err) {
284
284
  * "policies" that isn't a list and holds something. `runs` is what the gate
285
285
  * runs meanwhile (rulesRun).
286
286
  */
287
- function noListRefusal(noList, runs) {
287
+ function noListRefusal(noList, runs, path) {
288
288
  return `${noList}, so it has no list of rules a change could edit without writing over what it holds, and nothing `
289
- + `was saved. Meanwhile the gate runs ${runs}. Until gate.json is an object whose "policies" is a list holding its `
290
- + 'rules, a change is refused the same way.';
289
+ + `was saved. Meanwhile the gate runs ${runs}. Until gate.json, at ${path}, is an object whose "policies" is a list `
290
+ + 'holding its rules, a change is refused the same way.';
291
291
  }
292
292
  /**
293
293
  * A change's refusal where gate.json is there but couldn't be used as a file
@@ -295,15 +295,15 @@ function noListRefusal(noList, runs) {
295
295
  * written over it, so what it holds, often an operator's unfinished edit, stays
296
296
  * as it is. `runs` is what the gate runs meanwhile (rulesRun).
297
297
  */
298
- function unusableRefusal(unusable, runs) {
298
+ function unusableRefusal(unusable, runs, path) {
299
299
  if (!unusable.notJson) {
300
- return `gate.json couldn't be read (${unusable.error}), so a change can't edit it, and nothing was written over it. `
301
- + `Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json readable; until then, a change is `
302
- + 'refused the same way.';
300
+ return `gate.json, at ${path}, couldn't be read (${unusable.error}), so a change can't edit it, and nothing was written `
301
+ + `over it. Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json a file the gate can read; `
302
+ + 'until then, a change is refused the same way.';
303
303
  }
304
- return `gate.json isn't JSON the gate can read (${unusable.error}), so a change can't edit its rules without writing `
305
- + `over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change is `
306
- + 'refused the same way.';
304
+ return `gate.json, at ${path}, isn't JSON the gate can read (${unusable.error}), so a change can't edit its rules without `
305
+ + `writing over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change `
306
+ + 'is refused the same way.';
307
307
  }
308
308
  /**
309
309
  * A change's refusal at a start that didn't use gate.json, which has no rules
@@ -313,12 +313,17 @@ function unusableRefusal(unusable, runs) {
313
313
  * (notUsedClause, describeStart) say which rules keep gate.json out of use
314
314
  * and each fix.
315
315
  */
316
- function notUsedRefusal(runs) {
317
- 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 "
316
+ function notUsedRefusal(runs, path) {
317
+ 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 `
318
318
  + `force in place of what that runs, so nothing was saved. Meanwhile the gate runs ${runs}. gate_status says what keeps `
319
319
  + 'gate.json out of use and what would put it in use, such as giving it rules under "policies", or fixing the starting '
320
320
  + 'rules, which whoever runs this host can do, for the next start; until then, a change is refused the same way.';
321
321
  }
322
+ function savedOnlyInForce(path, remedy) {
323
+ return remedy === undefined
324
+ ? ''
325
+ : ` gate.json, at ${path}, isn't in force, and a change is saved only if it brings it into force. ${remedy}`;
326
+ }
322
327
  /**
323
328
  * A value as a message quotes it: its JSON, cut short when long, never inside a
324
329
  * surrogate pair, since the message reaches the agent's context.
@@ -331,7 +336,8 @@ export function shownValue(value) {
331
336
  return String(value);
332
337
  let json;
333
338
  try {
334
- json = JSON.stringify(value) ?? String(value);
339
+ const stringified = JSON.stringify(value);
340
+ json = stringified === undefined ? String(value) : quotedJson(stringified);
335
341
  }
336
342
  catch {
337
343
  json = String(value);
@@ -346,9 +352,22 @@ function shownValues(values) {
346
352
  function joinedList(items, joiner = 'and') {
347
353
  return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} ${joiner} ${items[items.length - 1]}`;
348
354
  }
355
+ const asGateNames = (path) => path;
356
+ function namedBy(names) {
357
+ if (!names)
358
+ return asGateNames;
359
+ return (path) => {
360
+ for (let end = path.length; end > 0; end = path.lastIndexOf('.', end - 1)) {
361
+ const head = path.slice(0, end);
362
+ if (Object.hasOwn(names, head))
363
+ return `${names[head]}${path.slice(end)}`;
364
+ }
365
+ return path;
366
+ };
367
+ }
349
368
  /** `"a"`, `"a" and "b"`, `"a", "b" and "c"` (or "or"). */
350
369
  function quotedList(names, joiner = 'and') {
351
- return joinedList(names.map((n) => `"${n}"`), joiner);
370
+ return joinedList(names.map((n) => quotedValue(n)), joiner);
352
371
  }
353
372
  /** 1st, 2nd, 3rd, 4th, 11th, 21st. */
354
373
  function ordinal(n) {
@@ -396,7 +415,7 @@ function fieldNames(obj, fields, held = true) {
396
415
  }
397
416
  /** `"tags" (did you mean "tagsAny", "tagsAll" or "tagsNone"?)` */
398
417
  function slipShown(slip) {
399
- return `"${slip.key}" (did you mean ${quotedList(slip.meant, 'or')}?)`;
418
+ return `${quotedValue(slip.key)} (did you mean ${quotedList(slip.meant, 'or')}?)`;
400
419
  }
401
420
  /**
402
421
  * An object's names that do nothing in its place, split into those that are
@@ -427,21 +446,21 @@ function misplacedShown(m) {
427
446
  * The levels of a gate.json the gate reads fields at, each as a message names
428
447
  * it, `file` naming the file itself.
429
448
  */
430
- function fieldLevels(file) {
449
+ function fieldLevels(file, named = asGateNames) {
431
450
  return [
432
451
  { name: file, fields: CONFIG_FIELDS },
433
452
  { name: 'a rule', fields: POLICY_FIELDS },
434
453
  { name: '"match"', fields: MATCH_FIELDS },
435
454
  { name: '"match.filter"', fields: FILTER_FIELDS },
436
455
  { name: '"behavior"', fields: BEHAVIOR_KINDS },
437
- { name: '"behavior.rate_limit"', fields: RATE_LIMIT_FIELDS },
438
- { name: '"behavior.passive_sample"', fields: PASSIVE_SAMPLE_FIELDS },
439
- { name: '"behavior.judge"', fields: JUDGE_FIELDS },
456
+ { name: `"${named('behavior.rate_limit')}"`, fields: RATE_LIMIT_FIELDS },
457
+ { name: `"${named('behavior.passive_sample')}"`, fields: PASSIVE_SAMPLE_FIELDS },
458
+ { name: `"${named('behavior.judge')}"`, fields: JUDGE_FIELDS },
440
459
  ];
441
460
  }
442
461
  /** Where a name is a field the gate reads, at a level other than the one whose fields are `here`. */
443
- function homesOf(file, here) {
444
- return (key) => fieldLevels(file)
462
+ function homesOf(file, here, named) {
463
+ return (key) => fieldLevels(file, named)
445
464
  .filter((level) => level.fields !== here && level.fields.includes(key))
446
465
  .map((level) => level.name);
447
466
  }
@@ -573,14 +592,15 @@ const DROPPED = 'so the rule is read without it';
573
592
  */
574
593
  function readFieldNames(r, level, obj, fields, readOf = () => SLIP_READ, misplacedReadOf = () => undefined) {
575
594
  const { slips, unread } = fieldNames(obj, fields);
576
- const { misplaced, rest } = placedElsewhere(unread, homesOf(r.file, fields));
577
- noteUnread(level ? `${r.where}: ${level}` : r.where, rest, fields, r.notes);
595
+ const { misplaced, rest } = placedElsewhere(unread, homesOf(r.file, fields, r.named));
596
+ const shown = level === null ? null : r.named(level);
597
+ noteUnread(shown ? `${r.where}: ${shown}` : r.where, rest, fields, r.notes);
578
598
  for (const slip of slips) {
579
- r.problems.push({ text: `${level ?? 'it'} has ${slipShown(slip)}, which the gate doesn't read`, read: readOf(slip) });
599
+ r.problems.push({ text: `${shown ?? 'it'} has ${slipShown(slip)}, which the gate doesn't read`, read: readOf(slip) });
580
600
  }
581
601
  for (const m of misplaced) {
582
602
  r.problems.push({
583
- text: `${level ?? 'it'} has ${misplacedShown(m)}, which the gate doesn't read here`,
603
+ text: `${shown ?? 'it'} has ${misplacedShown(m)}, which the gate doesn't read here`,
584
604
  read: misplacedReadOf(m) ?? (m.keys.length === 1 ? SLIP_READ : 'so they do nothing'),
585
605
  });
586
606
  }
@@ -593,7 +613,7 @@ function droppedEntries(count) {
593
613
  * A rule, read as written, or past each problem it has. `unnamed` names a rule
594
614
  * without a name in messages, and `file` the file it's in.
595
615
  */
596
- function readRule(raw, unnamed, file = 'gate.json') {
616
+ function readRule(raw, unnamed, file = 'gate.json', named = asGateNames) {
597
617
  if (!isPlainObject(raw)) {
598
618
  return {
599
619
  where: unnamed,
@@ -603,7 +623,7 @@ function readRule(raw, unnamed, file = 'gate.json') {
603
623
  };
604
624
  }
605
625
  const name = nameOf(raw);
606
- const r = { where: name ? `Policy "${name}"` : unnamed, file, problems: [], notes: [] };
626
+ const r = { where: name ? `Rule ${quotedValue(name)}` : unnamed, file, named, problems: [], notes: [] };
607
627
  const obj = withoutEmpty(r.where, raw, r.notes, RULE_LEFT_OUT);
608
628
  // Field names first, so a slip of one is named before what its absence does.
609
629
  readFieldNames(r, null, obj, POLICY_FIELDS, (slip) => {
@@ -629,7 +649,7 @@ function readRule(raw, unnamed, file = 'gate.json') {
629
649
  const { behavior, kind } = readBehavior(r, obj.behavior);
630
650
  const match = readMatch(r, obj.match);
631
651
  const resets = readResets(r, obj.resets);
632
- const passthrough = readPassthrough(r, obj.passthrough, behavior, kind);
652
+ const passthrough = readPassthrough(r, obj.passthrough, kind);
633
653
  const reading = {
634
654
  where: r.where, problems: r.problems, notes: r.notes, quiet: kind === 'skip' || kind === 'defer' || r.noTokens === true,
635
655
  };
@@ -685,7 +705,7 @@ function readBehavior(r, value) {
685
705
  }
686
706
  const behavior = READ_KIND[kind](r, b[kind]);
687
707
  for (const other of kinds.slice(1)) {
688
- const unread = { where: r.where, file: r.file, problems: [], notes: [] };
708
+ const unread = { where: r.where, file: r.file, named: r.named, problems: [], notes: [] };
689
709
  READ_KIND[other](unread, b[other]);
690
710
  for (const p of unread.problems)
691
711
  r.problems.push({ text: p.text, read: `which goes unread with the rest of "${other}"` });
@@ -701,18 +721,18 @@ function readBehavior(r, value) {
701
721
  */
702
722
  function readDebounce(r, ms) {
703
723
  if (typeof ms !== 'number' || !Number.isFinite(ms)) {
704
- r.problems.push({ text: `behavior.debounce is ${shownValue(ms)}, where a number of milliseconds is needed, such as 60000` });
724
+ r.problems.push({ text: `${r.named('behavior.debounce')} is ${shownValue(ms)}, where a number of milliseconds is needed, such as 60000` });
705
725
  return undefined;
706
726
  }
707
727
  if (ms < MIN_DEBOUNCE_MS) {
708
728
  r.problems.push({
709
- text: `behavior.debounce is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
729
+ 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`,
710
730
  });
711
731
  return { debounce: MIN_DEBOUNCE_MS };
712
732
  }
713
733
  if (ms > MAX_DEBOUNCE_MS) {
714
734
  r.problems.push({
715
- text: `behavior.debounce is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
735
+ 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`,
716
736
  });
717
737
  return { debounce: MAX_DEBOUNCE_MS };
718
738
  }
@@ -734,11 +754,11 @@ function readDebounce(r, ms) {
734
754
  function readRateLimit(r, value) {
735
755
  if (!isPlainObject(value)) {
736
756
  r.problems.push({
737
- text: `behavior.rate_limit is ${shownValue(value)}, where an object is needed, such as {"tokens": 3, "refillIntervalMs": 60000}`,
757
+ text: `${r.named('behavior.rate_limit')} is ${shownValue(value)}, where an object is needed, such as {"tokens": 3, "refillIntervalMs": 60000}`,
738
758
  });
739
759
  return undefined;
740
760
  }
741
- const conf = withoutEmpty(`${r.where}: behavior.rate_limit`, value, r.notes, { keyBy: 'every event the rule matches shares one bucket' });
761
+ const conf = withoutEmpty(`${r.where}: ${r.named('behavior.rate_limit')}`, value, r.notes, { keyBy: 'every event the rule matches shares one bucket' });
742
762
  readFieldNames(r, 'behavior.rate_limit', conf, RATE_LIMIT_FIELDS);
743
763
  const tokens = conf.tokens;
744
764
  const refill = conf.refillIntervalMs;
@@ -746,17 +766,17 @@ function readRateLimit(r, value) {
746
766
  if (typeof tokens === 'number' && !(tokens > 0))
747
767
  r.noTokens = true;
748
768
  if (!tokensUsable) {
749
- r.problems.push({ text: `behavior.rate_limit.tokens is ${shownValue(tokens)}, where a positive number is needed` });
769
+ r.problems.push({ text: `${r.named('behavior.rate_limit.tokens')} is ${shownValue(tokens)}, where a positive number is needed` });
750
770
  }
751
771
  const refillUsable = typeof refill === 'number' && Number.isFinite(refill);
752
772
  if (!refillUsable) {
753
773
  r.problems.push({
754
- text: `behavior.rate_limit.refillIntervalMs is ${shownValue(refill)}, where a positive number of milliseconds is needed`,
774
+ text: `${r.named('behavior.rate_limit.refillIntervalMs')} is ${shownValue(refill)}, where a positive number of milliseconds is needed`,
755
775
  });
756
776
  }
757
777
  else if (refill <= 0) {
758
778
  r.problems.push({
759
- text: `behavior.rate_limit.refillIntervalMs is ${refill}, where a positive number of milliseconds is needed`,
779
+ text: `${r.named('behavior.rate_limit.refillIntervalMs')} is ${refill}, where a positive number of milliseconds is needed`,
760
780
  read: refill === 0
761
781
  ? 'so its tokens refill at once, and it limits nothing it matches'
762
782
  : "so its tokens never refill, and once they're spent it wakes the agent on nothing it matches",
@@ -777,15 +797,15 @@ function readRateLimit(r, value) {
777
797
  */
778
798
  function readPassiveSample(r, value) {
779
799
  if (!isPlainObject(value)) {
780
- r.problems.push({ text: `behavior.passive_sample is ${shownValue(value)}, where an object is needed, such as {"every": 10}` });
800
+ r.problems.push({ text: `${r.named('behavior.passive_sample')} is ${shownValue(value)}, where an object is needed, such as {"every": 10}` });
781
801
  return undefined;
782
802
  }
783
- const conf = withoutEmpty(`${r.where}: behavior.passive_sample`, value, r.notes, { keyBy: 'every event the rule matches shares one count' });
803
+ const conf = withoutEmpty(`${r.where}: ${r.named('behavior.passive_sample')}`, value, r.notes, { keyBy: 'every event the rule matches shares one count' });
784
804
  readFieldNames(r, 'behavior.passive_sample', conf, PASSIVE_SAMPLE_FIELDS);
785
805
  const given = conf.every;
786
806
  let every;
787
807
  if (typeof given !== 'number' || !Number.isFinite(given)) {
788
- r.problems.push({ text: `behavior.passive_sample.every is ${shownValue(given)}, where a whole number of 1 or more is needed` });
808
+ r.problems.push({ text: `${r.named('behavior.passive_sample.every')} is ${shownValue(given)}, where a whole number of 1 or more is needed` });
789
809
  }
790
810
  else if (Number.isInteger(given) && given > 0) {
791
811
  every = given;
@@ -793,7 +813,7 @@ function readPassiveSample(r, value) {
793
813
  else {
794
814
  every = given <= 0 ? 1 : Math.ceil(given);
795
815
  r.problems.push({
796
- text: `behavior.passive_sample.every is ${given}, ${given <= 0 ? 'under 1' : 'not a whole number'}`,
816
+ text: `${r.named('behavior.passive_sample.every')} is ${given}, ${given <= 0 ? 'under 1' : 'not a whole number'}`,
797
817
  read: `so it's read as ${every}`,
798
818
  });
799
819
  }
@@ -817,11 +837,11 @@ function readPassiveSample(r, value) {
817
837
  function readJudge(r, value) {
818
838
  if (!isPlainObject(value)) {
819
839
  r.problems.push({
820
- text: `behavior.judge is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
840
+ text: `${r.named('behavior.judge')} is ${shownValue(value)}, where an object is needed, such as {"debounceMs": 30000, "instructions": "..."}`,
821
841
  });
822
842
  return undefined;
823
843
  }
824
- const conf = withoutEmpty(`${r.where}: behavior.judge`, value, r.notes, {
844
+ const conf = withoutEmpty(`${r.where}: ${r.named('behavior.judge')}`, value, r.notes, {
825
845
  maxWaitMs: 'a batch is judged only after its quiet period',
826
846
  instructions: 'the judge decides without the agent\'s word',
827
847
  });
@@ -830,18 +850,18 @@ function readJudge(r, value) {
830
850
  const ms = conf.debounceMs;
831
851
  let debounceMs;
832
852
  if (typeof ms !== 'number' || !Number.isFinite(ms)) {
833
- r.problems.push({ text: `behavior.judge.debounceMs is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
853
+ r.problems.push({ text: `${r.named('behavior.judge.debounceMs')} is ${shownValue(ms)}, where a number of milliseconds is needed, such as 30000` });
834
854
  ok = false;
835
855
  }
836
856
  else if (ms < MIN_DEBOUNCE_MS) {
837
857
  r.problems.push({
838
- text: `behavior.judge.debounceMs is ${ms} ms, under the ${MIN_DEBOUNCE_MS} ms minimum`, read: `so it's read as ${MIN_DEBOUNCE_MS} ms`,
858
+ 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`,
839
859
  });
840
860
  debounceMs = MIN_DEBOUNCE_MS;
841
861
  }
842
862
  else if (ms > MAX_DEBOUNCE_MS) {
843
863
  r.problems.push({
844
- text: `behavior.judge.debounceMs is ${ms} ms, over the ${MAX_DEBOUNCE_MS} ms maximum`, read: `so it's read as ${MAX_DEBOUNCE_MS} ms`,
864
+ 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`,
845
865
  });
846
866
  debounceMs = MAX_DEBOUNCE_MS;
847
867
  }
@@ -852,21 +872,21 @@ function readJudge(r, value) {
852
872
  const wait = conf.maxWaitMs;
853
873
  if (wait !== undefined) {
854
874
  if (typeof wait !== 'number' || !Number.isFinite(wait)) {
855
- r.problems.push({ text: `behavior.judge.maxWaitMs is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
875
+ r.problems.push({ text: `${r.named('behavior.judge.maxWaitMs')} is ${shownValue(wait)}, where a number of milliseconds is needed, such as 120000` });
856
876
  ok = false;
857
877
  }
858
878
  else {
859
879
  const floor = debounceMs ?? MIN_DEBOUNCE_MS;
860
880
  if (wait < floor) {
861
881
  r.problems.push({
862
- text: `behavior.judge.maxWaitMs is ${wait} ms, under the rule's ${floor} ms quiet period`,
882
+ text: `${r.named('behavior.judge.maxWaitMs')} is ${wait} ms, under the rule's ${floor} ms quiet period`,
863
883
  read: `so it's read as ${floor} ms`,
864
884
  });
865
885
  maxWaitMs = floor;
866
886
  }
867
887
  else if (wait > MAX_JUDGE_WAIT_MS) {
868
888
  r.problems.push({
869
- text: `behavior.judge.maxWaitMs is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
889
+ text: `${r.named('behavior.judge.maxWaitMs')} is ${wait} ms, over the ${MAX_JUDGE_WAIT_MS} ms maximum`,
870
890
  read: `so it's read as ${MAX_JUDGE_WAIT_MS} ms`,
871
891
  });
872
892
  maxWaitMs = MAX_JUDGE_WAIT_MS;
@@ -880,12 +900,12 @@ function readJudge(r, value) {
880
900
  let instructions;
881
901
  if (said !== undefined) {
882
902
  if (typeof said !== 'string') {
883
- r.problems.push({ text: `behavior.judge.instructions is ${shownValue(said)}, where text is needed` });
903
+ r.problems.push({ text: `${r.named('behavior.judge.instructions')} is ${shownValue(said)}, where text is needed` });
884
904
  ok = false;
885
905
  }
886
906
  else if (said.length > MAX_JUDGE_INSTRUCTIONS) {
887
907
  r.problems.push({
888
- text: `behavior.judge.instructions is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
908
+ text: `${r.named('behavior.judge.instructions')} is ${said.length} characters long, over the ${MAX_JUDGE_INSTRUCTIONS}-character maximum`,
889
909
  });
890
910
  ok = false;
891
911
  }
@@ -916,7 +936,7 @@ function readKeyBy(r, kind, value) {
916
936
  if (typeof value === 'string')
917
937
  return value;
918
938
  r.problems.push({
919
- text: `behavior.${kind}.keyBy is ${shownValue(value)}, where a field name is needed, such as "channelId"`,
939
+ text: `${r.named(`behavior.${kind}.keyBy`)} is ${shownValue(value)}, where a field name is needed, such as "channelId"`,
920
940
  read: `so it does nothing, and every event the rule matches shares one ${kind === 'rate_limit' ? 'bucket' : 'count'}`,
921
941
  });
922
942
  return undefined;
@@ -1040,7 +1060,10 @@ function readMatch(r, value) {
1040
1060
  match.filter = { type, pattern: f.pattern };
1041
1061
  }
1042
1062
  catch (e) {
1043
- r.problems.push({ text: `match.filter.pattern isn't a regular expression the gate can read (${errorText(e)})` });
1063
+ r.problems.push({
1064
+ text: `match.filter.pattern isn't a regular expression the gate can read (${quotedValue(errorText(e))}), so fix it, `
1065
+ + 'or give match.filter.type "text" to look for the pattern as plain text',
1066
+ });
1044
1067
  }
1045
1068
  }
1046
1069
  }
@@ -1081,7 +1104,7 @@ function readResets(r, value) {
1081
1104
  * always, defer, skip or a debounce it would be a no-op at best and a
1082
1105
  * misleading promise at worst, so it's refused rather than ignored.
1083
1106
  */
1084
- function readPassthrough(r, value, behavior, kind) {
1107
+ function readPassthrough(r, value, kind) {
1085
1108
  if (value === undefined || value === false)
1086
1109
  return false;
1087
1110
  if (value !== true) {
@@ -1090,9 +1113,10 @@ function readPassthrough(r, value, behavior, kind) {
1090
1113
  }
1091
1114
  if (kind === undefined || kind === 'rate_limit' || kind === 'passive_sample')
1092
1115
  return true;
1116
+ const given = kind === 'always' || kind === 'defer' || kind === 'skip' ? `behavior "${kind}"` : r.named(`behavior.${kind}`);
1093
1117
  r.problems.push({
1094
- text: `passthrough is only valid with rate_limit or passive_sample behaviors — for "${behavior ? formatBehavior(behavior) : kind}" `
1095
- + 'there is no non-firing match to fall through on',
1118
+ text: `passthrough is true, and only a ${r.named('behavior.rate_limit')} or ${r.named('behavior.passive_sample')} rule passes on `
1119
+ + `an event it matches without waking on it; with ${given}, leave passthrough out`,
1096
1120
  });
1097
1121
  return false;
1098
1122
  }
@@ -1223,7 +1247,7 @@ function readConfig(raw, file, start) {
1223
1247
  + `[{"name": "my-rule", "behavior": "always"}], and ${noRules}.`);
1224
1248
  }
1225
1249
  const rules = policies.map((rule, i) => {
1226
- const reading = readRule(rule, `The ${ordinal(i + 1)} policy`, file);
1250
+ const reading = readRule(rule, `The ${ordinal(i + 1)} rule`, file);
1227
1251
  return { raw: rule, reading, outcome: ruleOutcome(reading) };
1228
1252
  });
1229
1253
  if (defaultBehavior !== 'always') {
@@ -1294,7 +1318,7 @@ function readLastRecipe(path) {
1294
1318
  const rules = read.rules.map((rule) => rule.raw);
1295
1319
  const twice = duplicateName(rules);
1296
1320
  if (twice !== undefined)
1297
- return { recipe: null, problem: `it names the rule "${twice}" twice` };
1321
+ return { recipe: null, problem: `it names the rule ${quotedValue(twice)} twice` };
1298
1322
  return { recipe: { rules, default: isPlainObject(raw) ? raw.default : undefined } };
1299
1323
  }
1300
1324
  /**
@@ -1303,7 +1327,7 @@ function readLastRecipe(path) {
1303
1327
  * wrote as read agrees with the same rule written more briefly.
1304
1328
  */
1305
1329
  function ruleMeaning(rule) {
1306
- const reading = readRule(rule, 'A policy');
1330
+ const reading = readRule(rule, 'A rule');
1307
1331
  return reading.policy ? `read ${canonical(reading.policy)}` : `written ${canonical(rule)}`;
1308
1332
  }
1309
1333
  /**
@@ -1377,7 +1401,7 @@ function notUsedClause(write, starting) {
1377
1401
  */
1378
1402
  function outOfForce(write) {
1379
1403
  const nameless = write.nameless ?? 0;
1380
- const left = [...(write.leftOut ?? []).map((name) => `"${name}"`), ...(nameless === 0 ? [] : [nameless === 1 ? 'one with no name' : `${nameless} with no name`])];
1404
+ const left = [...(write.leftOut ?? []).map((name) => quotedValue(name)), ...(nameless === 0 ? [] : [nameless === 1 ? 'one with no name' : `${nameless} with no name`])];
1381
1405
  const held = write.heldOut ?? [];
1382
1406
  const said = [];
1383
1407
  const fix = [];
@@ -1605,14 +1629,14 @@ export function findShadowedPolicies(policies, eventTypesFrom) {
1605
1629
  * three it is (the rule never applies; it misses some of its events; it
1606
1630
  * may not be meant for those events at all) and what to do. */
1607
1631
  export function formatShadowWarning(w) {
1608
- const later = `"${w.later}"`;
1609
- const earlier = `"${w.earlier}"`;
1610
- const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: "${w.earlier}")`;
1632
+ const later = quotedValue(w.later);
1633
+ const earlier = quotedValue(w.earlier);
1634
+ const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: ${quotedValue(w.earlier)})`;
1611
1635
  if (w.whole) {
1612
1636
  return `${later} never applies: ${earlier} comes first and matches every event ${later} would. `
1613
1637
  + `If ${later} should apply, ${move}, or narrow ${earlier}.`;
1614
1638
  }
1615
- const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.join(', ');
1639
+ const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.map((type) => quotedValue(type).slice(1, -1)).join(', ');
1616
1640
  if (w.known) {
1617
1641
  return `${earlier} comes first and takes ${later}'s ${types} events, so ${later} applies only to its others. `
1618
1642
  + `If it should apply to those too, ${move}.`;
@@ -1644,20 +1668,23 @@ function afterChange(name, callback) {
1644
1668
  }
1645
1669
  }
1646
1670
  /** The privileged-users file's error when some entries of its list aren't user ids. */
1647
- function skippedPrivilegedIds(skipped, loaded) {
1671
+ function skippedPrivilegedIds(skipped, loaded, path) {
1648
1672
  const one = skipped.length === 1;
1649
1673
  const shown = skipped.slice(0, 5).map((x) => {
1650
1674
  if (typeof x === 'number' && Number.isInteger(x)) {
1651
1675
  return `${x} (a number this large can't be read exactly: write the id as a string, in quotes)`;
1652
1676
  }
1653
- const json = JSON.stringify(x) ?? String(x);
1677
+ const json = quotedValue(x);
1654
1678
  return json.length > 40 ? `${safeSlice(json, 0, 39)}…` : json;
1655
1679
  });
1656
1680
  const more = skipped.length > shown.length ? `, and ${skipped.length - shown.length} more` : '';
1657
- const rest = loaded === 0 ? 'no id loaded, so no one is privileged' : `${loaded} ${loaded === 1 ? 'id' : 'ids'} did load`;
1658
- return `${skipped.length} ${one ? 'entry' : 'entries'} in the privileged-users file ${one ? "isn't a user id" : "aren't user ids"}, `
1681
+ const rest = loaded === 0
1682
+ ? 'no id loaded, so no one is privileged'
1683
+ : `the ${loaded === 1 ? 'one id that did load is' : `${loaded} ids that did load are`} privileged`;
1684
+ return `${skipped.length} ${one ? 'entry' : 'entries'} in the privileged-users file, at ${path}, ${one ? "isn't a user id" : "aren't user ids"}, `
1659
1685
  + `so ${one ? 'it was' : 'they were'} skipped: ${shown.join(', ')}${more}. A user id is a non-empty string, or a safe integer `
1660
- + `(a whole number within 2^53 - 1 of zero); ${rest}.`;
1686
+ + `(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 `
1687
+ + 'reads the file again at its next check for changes.';
1661
1688
  }
1662
1689
  // ---------------------------------------------------------------------------
1663
1690
  // EventGate
@@ -1687,6 +1714,7 @@ export class EventGate {
1687
1714
  /** Whether gate.json's error is one that keeps what it holds out of force
1688
1715
  * (configUnreadable), so gate_status says what's in force instead. */
1689
1716
  configUnused = false;
1717
+ configFix = null;
1690
1718
  privilegedError = null;
1691
1719
  /** What the start did with the host's starting rules that it couldn't use
1692
1720
  * or add (describeStart), kept apart from gate.json's own error, until the
@@ -1759,8 +1787,8 @@ export class EventGate {
1759
1787
  sleepSuppressed = 0;
1760
1788
  sleepTimer = null;
1761
1789
  sleepAgent;
1762
- // From dispose() to the next resumeSleep(): no sleep or self-wake timer is
1763
- // armed, though a turn a failed stop() left running can still ask for one.
1790
+ // From dispose() on, no sleep or self-wake timer is armed, though a turn a
1791
+ // failed stop() left running can still ask for one.
1764
1792
  disposed = false;
1765
1793
  // Self-wake timers per agent (skip_reply's wake_in_seconds). Unlike sleep,
1766
1794
  // a self-wake suppresses NOTHING — it means "if nothing else wakes me by
@@ -1785,6 +1813,8 @@ export class EventGate {
1785
1813
  addMessageFn;
1786
1814
  requestInferenceFn;
1787
1815
  getAgentNamesFn;
1816
+ /** Delivery-time re-check: is this channel held from the recipients now? */
1817
+ isHeldFn;
1788
1818
  resolveRouteChannelFn;
1789
1819
  channelShownFn;
1790
1820
  isNewestEntryFn;
@@ -1798,6 +1828,7 @@ export class EventGate {
1798
1828
  this.addMessageFn = opts.addMessage;
1799
1829
  this.requestInferenceFn = opts.requestInference;
1800
1830
  this.getAgentNamesFn = opts.getAgentNames;
1831
+ this.isHeldFn = opts.isHeld ?? (() => false);
1801
1832
  this.resolveRouteChannelFn = opts.resolveRouteChannel ?? defaultRouteChannel;
1802
1833
  this.channelShownFn = opts.channelShown;
1803
1834
  this.isNewestEntryFn = opts.isNewestEntry;
@@ -1923,7 +1954,7 @@ export class EventGate {
1923
1954
  }
1924
1955
  const twice = duplicateName(starting.rules.map((rule) => rule.raw));
1925
1956
  if (twice !== undefined) {
1926
- this.startMergeNote = `At start, the recipe names the rule "${twice}" twice, so gate.json was seeded with it as given and it `
1957
+ this.startMergeNote = `At start, the recipe names the rule ${quotedValue(twice)} twice, so gate.json was seeded with it as given and it `
1927
1958
  + "wasn't recorded as the recipe last applied: a later start merges the recipe by name, once gate.json and the recipe "
1928
1959
  + 'name each rule once.';
1929
1960
  }
@@ -2119,8 +2150,10 @@ export class EventGate {
2119
2150
  raw = readFileSync(this.configPath, 'utf-8');
2120
2151
  }
2121
2152
  catch (err) {
2122
- if (!isMissing(err))
2123
- return this.configUnreadable(err);
2153
+ if (!isMissing(err)) {
2154
+ const why = quotedValue(errorText(err));
2155
+ return this.configUnreadable(why, `gate.json, at ${this.configPath}, can't be read: ${why}`, '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.');
2156
+ }
2124
2157
  // No file is no problem with it: the rules in force stay.
2125
2158
  this.configError = null;
2126
2159
  this.configUnused = false;
@@ -2140,12 +2173,22 @@ export class EventGate {
2140
2173
  parsed = parseGateJson(raw);
2141
2174
  }
2142
2175
  catch (err) {
2143
- return { ...this.configUnreadable(err), notJson: true };
2176
+ const why = quotedValue(errorText(err));
2177
+ return {
2178
+ ...this.configUnreadable(why, `gate.json, at ${this.configPath}, isn't JSON: ${why}`, "Once it's JSON, the gate reads it again at its next check for changes."),
2179
+ notJson: true,
2180
+ };
2144
2181
  }
2145
2182
  const read = readConfig(parsed, 'gate.json');
2146
2183
  // JSON the gate doesn't use is still the operator's file, edited as written.
2147
2184
  if ('refused' in read) {
2148
- return { ...this.configUnreadable(new Error(read.refused)), state: 'refused', listed: read.listed, silenced: read.silenced };
2185
+ const problem = read.refused.startsWith('gate.json ')
2186
+ ? `gate.json, at ${this.configPath},${read.refused.slice('gate.json'.length)}`
2187
+ : read.refused;
2188
+ return {
2189
+ ...this.configUnreadable(read.refused, problem, 'Once gate.json is mended, the gate reads it again at its next check for changes.'),
2190
+ state: 'refused', listed: read.listed, silenced: read.silenced,
2191
+ };
2149
2192
  }
2150
2193
  // A field the gate doesn't read does nothing, and a rule is read past a
2151
2194
  // problem or left out, so the file loads, and each is named.
@@ -2158,20 +2201,20 @@ export class EventGate {
2158
2201
  * gate.json's current error, replacing the last one, and traced and logged
2159
2202
  * when it's new, so an edit that doesn't load is in the log once.
2160
2203
  */
2161
- configUnreadable(err) {
2162
- const msg = err instanceof Error ? err.message : String(err);
2163
- if (msg !== this.configError) {
2164
- console.error(`[gate] ${this.configPath} couldn't be used (${msg}), so what it holds isn't in force`);
2204
+ configUnreadable(why, problem, fix = null) {
2205
+ if (problem !== this.configError) {
2206
+ console.error(`[gate] ${this.configPath} couldn't be used (${why}), so what it holds isn't in force${fix ? `. ${fix}` : ''}`);
2165
2207
  this.emitTrace({
2166
2208
  type: 'gate:config-error',
2167
- error: msg,
2209
+ error: why,
2168
2210
  configPath: this.configPath,
2169
2211
  timestamp: Date.now(),
2170
2212
  });
2171
2213
  }
2172
- this.configError = msg;
2214
+ this.configError = problem;
2215
+ this.configFix = fix;
2173
2216
  this.configUnused = true;
2174
- return { state: 'unreadable', error: msg };
2217
+ return { state: 'unreadable', error: why };
2175
2218
  }
2176
2219
  /**
2177
2220
  * gate.json's error as gate_status shows it: one that keeps what the file
@@ -2180,7 +2223,8 @@ export class EventGate {
2180
2223
  configErrorShown() {
2181
2224
  if (this.configError === null || !this.configUnused)
2182
2225
  return this.configError;
2183
- return `${sentence(this.configError)} So what gate.json holds isn't in force, and the gate runs ${this.rulesRun()}.`;
2226
+ return `${sentence(this.configError)} So what gate.json holds isn't in force, and the gate runs ${this.rulesRun()}.`
2227
+ + (this.configFix ? ` ${this.configFix}` : '');
2184
2228
  }
2185
2229
  /** What the gate runs, by where its rules came from, for a text that says so: "the host's starting rules". */
2186
2230
  rulesRun() {
@@ -2223,14 +2267,14 @@ export class EventGate {
2223
2267
  rulesForChange() {
2224
2268
  const read = this.readConfigFile();
2225
2269
  if (read.state === 'unreadable')
2226
- return { refused: unusableRefusal(read, this.rulesRun()) };
2270
+ return { refused: unusableRefusal(read, this.rulesRun(), this.configPath) };
2227
2271
  if (read.state === 'loaded' || read.state === 'refused') {
2228
2272
  if ('noList' in read.listed)
2229
- return { refused: noListRefusal(read.listed.noList, this.rulesRun()) };
2273
+ return { refused: noListRefusal(read.listed.noList, this.rulesRun(), this.configPath) };
2230
2274
  // Rules not gate.json's in force while it holds the text in force: what a
2231
2275
  // start ran in place of one with no rules of its own, until it changes.
2232
2276
  if (read.state === 'loaded' && read.text === this.configText && this.configSource !== 'file') {
2233
- return { refused: notUsedRefusal(this.rulesRun()) };
2277
+ return { refused: notUsedRefusal(this.rulesRun(), this.configPath) };
2234
2278
  }
2235
2279
  const edit = { top: read.listed.top, rules: [...read.listed.rules] };
2236
2280
  if (read.state === 'loaded')
@@ -2248,9 +2292,9 @@ export class EventGate {
2248
2292
  return {
2249
2293
  ...edit,
2250
2294
  remedy: sharing === 1
2251
- ? `${removes ? 'Replacing or removing' : 'Replacing'} "${only}" by name would bring gate.json into force.`
2295
+ ? `${removes ? 'Replacing or removing' : 'Replacing'} ${quotedValue(only)} by name would bring gate.json into force.`
2252
2296
  : sharing > 1
2253
- ? `gate.json has ${sharing} rules named "${only}", and a change by that name replaces the first of them`
2297
+ ? `gate.json has ${sharing} rules named ${quotedValue(only)}, and a change by that name replaces the first of them`
2254
2298
  + `${removes ? ' or removes them all' : ''}.`
2255
2299
  : 'No change through the gate can bring it into force, so gate.json has to be fixed by an edit of its own; '
2256
2300
  + 'until then, a change is refused the same way.',
@@ -2424,7 +2468,8 @@ export class EventGate {
2424
2468
  * part-written.
2425
2469
  */
2426
2470
  addPolicy(rawPolicy, options) {
2427
- const reading = readRule(rawPolicy, 'A policy');
2471
+ const named = namedBy(options?.fieldNames);
2472
+ const reading = readRule(rawPolicy, 'A rule', 'gate.json', named);
2428
2473
  if (reading.problems.length > 0 || !reading.policy)
2429
2474
  throw new Error(refusalOf(reading));
2430
2475
  const policy = reading.policy;
@@ -2448,8 +2493,23 @@ export class EventGate {
2448
2493
  const anchorName = 'before' in position ? position.before : position.after;
2449
2494
  const anchorIdx = rules.findIndex((rule) => nameOf(rule) === anchorName);
2450
2495
  if (anchorIdx < 0) {
2451
- throw new Error(`addPolicy: no policy named "${anchorName}" to anchor on — `
2452
- + `have: [${rules.map(nameOf).filter((name) => name !== undefined).join(', ')}]`);
2496
+ const field = named('before' in position ? 'position.before' : 'position.after');
2497
+ const others = [...new Set(rules.map(nameOf).filter((name) => name !== undefined))];
2498
+ if (idx >= 0 && anchorName === policy.name) {
2499
+ throw new Error(`${reading.where} wasn't saved: ${field} is ${quotedValue(anchorName)}, the rule being saved, and a rule can't be `
2500
+ + 'placed beside itself. '
2501
+ + (others.length > 0
2502
+ ? `The other rules are ${quotedList(others)}. Give ${field} one of those to place the rule beside it, or leave it out`
2503
+ : `There are no other rules, so leave ${field} out`)
2504
+ + ' to replace the rule where it is.'
2505
+ + savedOnlyInForce(this.configPath, remedy));
2506
+ }
2507
+ throw new Error(`${reading.where} wasn't saved: ${field} is ${quotedValue(anchorName)}, and no rule has that name. `
2508
+ + (others.length > 0
2509
+ ? `The other rules are ${quotedList(others)}. Give ${field} one of those to place the rule beside it, or leave it out`
2510
+ : `There are no other rules, so leave ${field} out`)
2511
+ + ' to add the rule at the end, or, where a rule already has its name, to replace that rule where it is.'
2512
+ + savedOnlyInForce(this.configPath, remedy));
2453
2513
  }
2454
2514
  rules.splice('before' in position ? anchorIdx : anchorIdx + 1, 0, policy);
2455
2515
  }
@@ -2525,6 +2585,16 @@ export class EventGate {
2525
2585
  const current = read.state === 'loaded' ? read.config : this.config;
2526
2586
  return current.policies.map((p) => p.name);
2527
2587
  }
2588
+ /** @internal For `wake_add_rule`'s and `wake_remove_rule`'s refusals; not a host API. */
2589
+ ruleNamesForChange() {
2590
+ const edit = this.rulesForChange();
2591
+ if ('refused' in edit)
2592
+ return { refused: edit.refused };
2593
+ return {
2594
+ names: [...new Set(edit.rules.map(nameOf).filter((name) => name !== undefined))],
2595
+ note: savedOnlyInForce(this.configPath, edit.remedy),
2596
+ };
2597
+ }
2528
2598
  /**
2529
2599
  * Persist gate.json and apply it in memory together: `top`, its own fields,
2530
2600
  * with `rules` as its rule list. Only what loads is written: a change to a
@@ -2550,13 +2620,14 @@ export class EventGate {
2550
2620
  const text = JSON.stringify({ ...top, policies: rules }, null, 2) + '\n';
2551
2621
  const read = readConfig(JSON.parse(text), 'gate.json');
2552
2622
  if ('refused' in read) {
2553
- throw new Error(`Nothing was saved: with this change, gate.json wouldn't be used. ${sentence(read.refused)}${remedy ? ` ${remedy}` : ''}`);
2623
+ throw new Error(`Nothing was saved: with this change, gate.json, at ${this.configPath}, wouldn't be used. ${sentence(read.refused)}`
2624
+ + (remedy ? ` ${remedy}` : ''));
2554
2625
  }
2555
2626
  // One the gate doesn't use, and that a change can mend, holds back what a
2556
2627
  // rule it leaves out would wake on, so its default holds events back.
2557
2628
  if (remedy !== undefined && read.config.policies.length === 0) {
2558
- throw new Error('Nothing was saved: with this change, gate.json would have no rules the gate reads, and its default would hold back every '
2559
- + `event. Meanwhile the gate runs ${this.rulesRun()}. ${remedy}`);
2629
+ throw new Error(`Nothing was saved: with this change, gate.json, at ${this.configPath}, would have no rules the gate reads, and its default `
2630
+ + `would hold back every event. Meanwhile the gate runs ${this.rulesRun()}. ${remedy}`);
2560
2631
  }
2561
2632
  try {
2562
2633
  mkdirSync(dirname(this.configPath), { recursive: true });
@@ -3187,7 +3258,7 @@ export class EventGate {
3187
3258
  }
3188
3259
  catch (err) {
3189
3260
  const one = events.length === 1;
3190
- console.error(`[gate] Policy "${policyName}": its pending batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
3261
+ console.error(`[gate] Rule ${quotedValue(policyName)}: its pending batch of ${events.length} ${one ? 'event' : 'events'} couldn't be delivered `
3191
3262
  + `(${errorText(err)}), so the batch is dropped`);
3192
3263
  return;
3193
3264
  }
@@ -3201,7 +3272,23 @@ export class EventGate {
3201
3272
  // =========================================================================
3202
3273
  // Event delivery
3203
3274
  // =========================================================================
3204
- deliverEvents(events) {
3275
+ deliverEvents(batched) {
3276
+ // Delivery-time re-check: a channel tuned out after its events were
3277
+ // batched is held now, and "already shown above at arrival" would be
3278
+ // untrue of them. They are dropped; the hold's own reader has them.
3279
+ const heldAt = (e) => e.routeChannelId ?? e.channelId;
3280
+ const events = batched.filter((e) => {
3281
+ const channel = heldAt(e);
3282
+ return !(channel && this.isHeldFn(e.serverId, channel));
3283
+ });
3284
+ if (events.length < batched.length) {
3285
+ this.emitTrace({
3286
+ type: 'gate:held-dropped',
3287
+ dropped: batched.length - events.length,
3288
+ channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
3289
+ timestamp: this.now(),
3290
+ });
3291
+ }
3205
3292
  if (events.length === 0)
3206
3293
  return;
3207
3294
  const policyNames = [...new Set(events.map(e => e.policyName))];
@@ -3361,7 +3448,7 @@ export class EventGate {
3361
3448
  if (state.timer)
3362
3449
  clearTimeout(state.timer);
3363
3450
  }).catch((err) => {
3364
- console.error(`[gate] Policy "${policyName}": the judge's verdict couldn't be applied (${errorText(err)})`);
3451
+ console.error(`[gate] Rule ${quotedValue(policyName)}: the judge's verdict couldn't be applied (${errorText(err)})`);
3365
3452
  });
3366
3453
  }
3367
3454
  /** Hold a batch for the judge until no turn changing the context it reads
@@ -3429,8 +3516,8 @@ export class EventGate {
3429
3516
  const n = state.events.length;
3430
3517
  const batch = `${n} ${n === 1 ? 'event' : 'events'}`;
3431
3518
  wakes.push(state.judge
3432
- ? { at: state.dueAt, certain: false, what: `rule "${name}"'s batch of ${batch} goes to the judge` }
3433
- : { at: state.dueAt, certain: true, what: `rule "${name}"'s debounced batch of ${batch}` });
3519
+ ? { at: state.dueAt, certain: false, what: `rule ${quotedValue(name)}'s batch of ${batch} goes to the judge` }
3520
+ : { at: state.dueAt, certain: true, what: `rule ${quotedValue(name)}'s debounced batch of ${batch}` });
3434
3521
  }
3435
3522
  return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
3436
3523
  }
@@ -3480,7 +3567,7 @@ export class EventGate {
3480
3567
  const failed = verdict == null || typeof verdict !== 'object' || typeof verdict.reply !== 'boolean';
3481
3568
  if (failed && err?.providerAdmission === 'deferred') {
3482
3569
  const wakes = this.judgeOnError === 'wake';
3483
- console.error(`[gate] Policy "${policyName}": the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
3570
+ console.error(`[gate] Rule ${quotedValue(policyName)}: the reply judge made no call (${errorText(err)}), so its batch of ${state.events.length} `
3484
3571
  + `${state.events.length === 1 ? 'event' : 'events'} ${wakes ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`);
3485
3572
  if (this.disposed)
3486
3573
  return;
@@ -3515,7 +3602,7 @@ export class EventGate {
3515
3602
  }
3516
3603
  const wake = outcome === 'reply' || (outcome === 'error' && this.judgeOnError === 'wake');
3517
3604
  if (failed) {
3518
- console.error(`[gate] Policy "${policyName}": the reply judge failed (${reason}), so its batch of ${state.events.length} `
3605
+ console.error(`[gate] Rule ${quotedValue(policyName)}: the reply judge failed (${reason}), so its batch of ${state.events.length} `
3519
3606
  + `${state.events.length === 1 ? 'event' : 'events'} ${wake ? 'wakes the agent' : "doesn't wake the agent"} (judge onError "${this.judgeOnError}")`);
3520
3607
  }
3521
3608
  this.emitTrace({
@@ -3606,11 +3693,10 @@ export class EventGate {
3606
3693
  }
3607
3694
  /** A turn that continues another (a budget restart, a tool-result-guard
3608
3695
  * retry) holds the entry its predecessor kept for it, and takes it again
3609
- * where that entry ended in between: at a stop(), whose dispose() clears
3610
- * every entry, or at a provider hold that kept the turn from starting,
3611
- * whose end queues it again. Unlike onInferenceStarted it leaves a
3612
- * pending self-wake alone: the logical turn that armed it is the one
3613
- * still running. */
3696
+ * where that entry ended in between, as at a provider hold that kept the
3697
+ * turn from starting, whose end queues it again. Unlike
3698
+ * onInferenceStarted it leaves a pending self-wake alone: the logical
3699
+ * turn that armed it is the one still running. */
3614
3700
  onInferenceResumed(agentName) {
3615
3701
  this.inferring.add(agentName);
3616
3702
  if (this.changesJudgedContext(agentName))
@@ -3720,7 +3806,7 @@ export class EventGate {
3720
3806
  // Arm the self-wake: when the window elapses, clear sleep and request an
3721
3807
  // inference so the agent resumes on its own (not dependent on a heartbeat
3722
3808
  // tick or an incoming message). Cancelled by clearSleep()/`wake`, and by
3723
- // dispose(), after which start() arms it again (resumeSleep).
3809
+ // dispose(), after which nothing arms it.
3724
3810
  this.armSleepWake();
3725
3811
  this.reloadPrivilegedIfChanged();
3726
3812
  this.emitTrace({
@@ -3733,25 +3819,11 @@ export class EventGate {
3733
3819
  });
3734
3820
  return { until: this.sleepUntil };
3735
3821
  }
3736
- /**
3737
- * Arm timers again for a lifecycle that starts after a stop(), and the
3738
- * sleep's wake first: dispose() clears its timer and keeps the sleep. A
3739
- * sleep still ahead wakes the agent at its end; one that ended while
3740
- * stopped wakes it now, since that wake is what the agent asked for. With
3741
- * no sleep, wakeFromSleep does nothing.
3742
- */
3743
- resumeSleep() {
3744
- this.disposed = false;
3745
- if (this.sleepUntil > this.now())
3746
- this.armSleepWake();
3747
- else
3748
- this.wakeFromSleep('sleep-expired');
3749
- }
3750
3822
  /**
3751
3823
  * Arm the sleep's wake for its end by the gate's own clock. One timer holds
3752
3824
  * at most MAX_TIMER_MS, about 24.8 days, so a longer sleep waits in steps,
3753
3825
  * and wakeFromSleep reads the clock as each one fires. Nothing is armed
3754
- * while the gate is disposed: resumeSleep() arms it at the next start().
3826
+ * once the gate is disposed.
3755
3827
  */
3756
3828
  armSleepWake() {
3757
3829
  if (this.sleepTimer) {
@@ -3764,10 +3836,9 @@ export class EventGate {
3764
3836
  this.sleepTimer = setTimeout(() => this.wakeFromSleep('sleep-expired'), Math.min(remaining, MAX_TIMER_MS));
3765
3837
  }
3766
3838
  /**
3767
- * `agentName` is gone, as a conversation fork's end or an ephemeral run's
3768
- * removes one: its pending self-wake is cancelled, rather than wake a name
3769
- * that no longer exists. It holds no sleep: neither can set one (the
3770
- * framework refuses their `sleep`).
3839
+ * `agentName` is gone, as an agent's end removes one: its pending
3840
+ * self-wake is cancelled, and a sleep it set ends, rather than go on
3841
+ * quieting every agent and then wake a name that no longer exists.
3771
3842
  */
3772
3843
  agentGone(agentName) {
3773
3844
  const selfWake = this.selfWakeTimers.get(agentName);
@@ -3775,6 +3846,8 @@ export class EventGate {
3775
3846
  clearTimeout(selfWake);
3776
3847
  this.selfWakeTimers.delete(agentName);
3777
3848
  }
3849
+ if (this.sleepAgent === agentName)
3850
+ this.clearSleep();
3778
3851
  }
3779
3852
  // =========================================================================
3780
3853
  // Self-wake (skip_reply wake_in_seconds)
@@ -3858,9 +3931,7 @@ export class EventGate {
3858
3931
  * fires early (Node's can, by a millisecond), or a clock set back.
3859
3932
  */
3860
3933
  wakeFromSleep(reason) {
3861
- // The timer that called it has fired; from resumeSleep(), for a sleep
3862
- // already over, one may still be armed, and it goes, so no stale step of
3863
- // it can fire later.
3934
+ // The timer that called it has fired.
3864
3935
  if (this.sleepTimer) {
3865
3936
  clearTimeout(this.sleepTimer);
3866
3937
  this.sleepTimer = null;
@@ -3923,15 +3994,21 @@ export class EventGate {
3923
3994
  * each a non-empty string or a safe integer (a whole number within 2^53 - 1 of zero). An absent file
3924
3995
  * means no one is privileged. A file that can't be read, isn't JSON, or
3925
3996
  * holds no list leaves the list that last loaded in this process in effect
3926
- * (none, at startup), with its error in gate_status until the file is read
3927
- * again. In a list, an entry that isn't a user id is skipped, and named in
3928
- * the file's error, and the rest load. With `unlessUnchanged`, a file whose
3997
+ * (none, at startup), with its error in gate_status until a later read
3998
+ * loads it, finds it gone, or fails another way. In a list, an entry that isn't a user id is skipped, and named
3999
+ * in the file's error, and the rest load. With `unlessUnchanged`, a file whose
3929
4000
  * text is the text last read isn't parsed again.
3930
4001
  */
3931
4002
  loadPrivileged(opts) {
3932
4003
  if (!this.privilegedUsersPath)
3933
4004
  return;
3934
- const failed = (why) => `Failed to load privileged-users file: ${why}`;
4005
+ const file = `The privileged-users file, at ${this.privilegedUsersPath},`;
4006
+ const held = this.privilegedUserIds.size;
4007
+ const meanwhile = held === 0
4008
+ ? 'no one is privileged'
4009
+ : `${held === 1 ? 'the one id of the list that last loaded stays' : `the ${held} ids of the list that last loaded stay`} privileged`;
4010
+ const revoke = 'Replacing it with a file holding [], or removing it, makes no one privileged at the gate\'s next check for changes.';
4011
+ const failed = (problem, fix) => `${file} ${problem}, so ${meanwhile}. ${fix} ${revoke}`;
3935
4012
  let raw;
3936
4013
  try {
3937
4014
  raw = readFileSync(this.privilegedUsersPath, 'utf8');
@@ -3945,26 +4022,27 @@ export class EventGate {
3945
4022
  this.privilegedError = null;
3946
4023
  }
3947
4024
  else {
3948
- this.privilegedError = failed(err instanceof Error ? err.message : String(err));
4025
+ this.privilegedError = failed(`can't be read: ${quotedValue(errorText(err))}`, '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.');
3949
4026
  }
3950
4027
  return;
3951
4028
  }
3952
4029
  if (opts?.unlessUnchanged && raw === this.privilegedText)
3953
4030
  return;
3954
4031
  this.privilegedText = raw;
4032
+ const fixed = 'Once it holds a list of user ids, the gate loads it at its next check for changes.';
3955
4033
  let ids;
3956
4034
  try {
3957
4035
  const parsed = parseGateJson(raw);
3958
4036
  ids = Array.isArray(parsed) ? parsed : parsed?.userIds;
3959
4037
  }
3960
4038
  catch (err) {
3961
- this.privilegedError = failed(err instanceof Error ? err.message : String(err));
4039
+ this.privilegedError = failed(`isn't JSON: ${quotedValue(errorText(err))}`, fixed);
3962
4040
  return;
3963
4041
  }
3964
4042
  // A file that parses but holds no list hasn't loaded either: it used to
3965
4043
  // mean no one is privileged, without a word.
3966
4044
  if (!Array.isArray(ids)) {
3967
- this.privilegedError = failed('expected a list of user ids, or {"userIds": [...]}');
4045
+ this.privilegedError = failed('holds no list of user ids, where one is needed, such as ["123"] or {"userIds": ["123"]}', fixed);
3968
4046
  return;
3969
4047
  }
3970
4048
  const loaded = new Set();
@@ -3980,7 +4058,7 @@ export class EventGate {
3980
4058
  skipped.push(id);
3981
4059
  }
3982
4060
  this.privilegedUserIds = loaded;
3983
- this.privilegedError = skipped.length > 0 ? skippedPrivilegedIds(skipped, loaded.size) : null;
4061
+ this.privilegedError = skipped.length > 0 ? skippedPrivilegedIds(skipped, loaded.size, this.privilegedUsersPath) : null;
3984
4062
  }
3985
4063
  /** At most once a second, the privileged-users file read again, and loaded when its text has changed. */
3986
4064
  reloadPrivilegedIfChanged() {
@@ -4134,9 +4212,9 @@ export class EventGate {
4134
4212
  this.selfWakeDue.clear();
4135
4213
  // The sleep stays, with its note and agent, and its timer goes: fired
4136
4214
  // after a stop(), its wake reached a framework that had stopped, and the
4137
- // timer held the process until the sleep's end. The next start() arms it
4138
- // again (resumeSleep). Until then nothing arms one, nor a self-wake: a
4139
- // turn a failed stop() left running can still call sleep or skip_reply.
4215
+ // timer held the process until the sleep's end. Nothing arms one after
4216
+ // this, nor a self-wake: a turn a failed stop() left running can still
4217
+ // call sleep or skip_reply.
4140
4218
  if (this.sleepTimer) {
4141
4219
  clearTimeout(this.sleepTimer);
4142
4220
  this.sleepTimer = null;
@@ -4149,13 +4227,9 @@ export class EventGate {
4149
4227
  this.inferenceBuffer = [];
4150
4228
  // stop() disposes the gate once its streams and event loop have ended (a
4151
4229
  // turn still compiling then is dropped at its next check, before it sends
4152
- // anything), and a start() goes on with this gate. An agent left here by
4153
- // a turn whose exit didn't end it (a retry stop() cancelled, a successor
4154
- // stop() refused) would make every debounced batch buffer until that
4155
- // agent's next turn ended. A turn that continues after the start() takes
4156
- // its entry again (onInferenceResumed). A start() made during the stop()
4157
- // is refused, so no turn of a new lifecycle can have begun before this
4158
- // runs.
4230
+ // anything). An agent left here by a turn whose exit didn't end it (a
4231
+ // retry stop() cancelled, a successor stop() refused) would make every
4232
+ // debounced batch buffer until that agent's next turn ended.
4159
4233
  this.inferring.clear();
4160
4234
  }
4161
4235
  }