@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.
- package/README.md +71 -8
- package/dist/src/admission/admit.d.ts +90 -0
- package/dist/src/admission/admit.d.ts.map +1 -0
- package/dist/src/admission/admit.js +43 -0
- package/dist/src/admission/admit.js.map +1 -0
- package/dist/src/agent.d.ts +50 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +125 -23
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/author-presentation.d.ts +50 -0
- package/dist/src/author-presentation.d.ts.map +1 -0
- package/dist/src/author-presentation.js +58 -0
- package/dist/src/author-presentation.js.map +1 -0
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/dendrite/index.d.ts +6 -0
- package/dist/src/dendrite/index.d.ts.map +1 -0
- package/dist/src/dendrite/index.js +3 -0
- package/dist/src/dendrite/index.js.map +1 -0
- package/dist/src/dendrite/presets.d.ts +60 -0
- package/dist/src/dendrite/presets.d.ts.map +1 -0
- package/dist/src/dendrite/presets.js +117 -0
- package/dist/src/dendrite/presets.js.map +1 -0
- package/dist/src/dendrite/registry.d.ts +260 -0
- package/dist/src/dendrite/registry.d.ts.map +1 -0
- package/dist/src/dendrite/registry.js +742 -0
- package/dist/src/dendrite/registry.js.map +1 -0
- package/dist/src/dendrite/types.d.ts +392 -0
- package/dist/src/dendrite/types.d.ts.map +1 -0
- package/dist/src/dendrite/types.js +24 -0
- package/dist/src/dendrite/types.js.map +1 -0
- package/dist/src/framework.d.ts +806 -118
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4180 -1121
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +36 -21
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +223 -149
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts +4 -0
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +12 -10
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +84 -9
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +136 -58
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +11 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +21 -0
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +7 -0
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/notice-tags.d.ts +2 -0
- package/dist/src/notice-tags.d.ts.map +1 -1
- package/dist/src/notice-tags.js +2 -0
- package/dist/src/notice-tags.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +4 -2
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +7 -4
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/stored-injections.d.ts +4 -0
- package/dist/src/stored-injections.d.ts.map +1 -1
- package/dist/src/stored-injections.js +6 -0
- package/dist/src/stored-injections.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +368 -16
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1224 -136
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +115 -7
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js +55 -18
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +153 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +140 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +138 -31
- package/src/api/mcp-server-core.ts +10 -3
- package/src/author-presentation.ts +81 -0
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/dendrite/index.ts +32 -0
- package/src/dendrite/presets.ts +149 -0
- package/src/dendrite/registry.ts +808 -0
- package/src/dendrite/types.ts +397 -0
- package/src/framework.ts +4344 -1052
- package/src/gate/event-gate.ts +251 -150
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +14 -10
- package/src/index.ts +16 -2
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/channel-registry.ts +170 -60
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +21 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/toolsets-module.ts +7 -0
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/notice-tags.ts +2 -0
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/silent-heartbeat.ts +7 -4
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +1374 -141
- package/src/tune-out/tools.ts +166 -13
- package/src/types/agent.ts +30 -1
- package/src/types/framework.ts +157 -15
- package/src/types/module.ts +42 -3
- 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
|
|
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
|
|
301
|
-
+ `Meanwhile the gate runs ${runs}. Whoever runs this host can make gate.json
|
|
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
|
|
305
|
-
+ `over what it holds, and nothing was saved. Meanwhile the gate runs ${runs}. Until gate.json is JSON, a change
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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: '
|
|
438
|
-
{ name: '
|
|
439
|
-
{ name: '
|
|
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
|
-
|
|
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: `${
|
|
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: `${
|
|
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 ? `
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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({
|
|
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,
|
|
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
|
|
1095
|
-
+
|
|
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)}
|
|
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
|
|
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
|
|
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) =>
|
|
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 =
|
|
1609
|
-
const earlier =
|
|
1610
|
-
const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore:
|
|
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 =
|
|
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
|
|
1658
|
-
|
|
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()
|
|
1763
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2162
|
-
|
|
2163
|
-
|
|
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:
|
|
2209
|
+
error: why,
|
|
2168
2210
|
configPath: this.configPath,
|
|
2169
2211
|
timestamp: Date.now(),
|
|
2170
2212
|
});
|
|
2171
2213
|
}
|
|
2172
|
-
this.configError =
|
|
2214
|
+
this.configError = problem;
|
|
2215
|
+
this.configFix = fix;
|
|
2173
2216
|
this.configUnused = true;
|
|
2174
|
-
return { state: 'unreadable', error:
|
|
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'}
|
|
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
|
|
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
|
|
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
|
-
|
|
2452
|
-
|
|
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)}
|
|
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(
|
|
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]
|
|
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(
|
|
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]
|
|
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
|
|
3433
|
-
: { at: state.dueAt, certain: true, what: `rule
|
|
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]
|
|
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]
|
|
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
|
|
3610
|
-
*
|
|
3611
|
-
*
|
|
3612
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
3768
|
-
*
|
|
3769
|
-
*
|
|
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
|
|
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
|
|
3927
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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('
|
|
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.
|
|
4138
|
-
//
|
|
4139
|
-
//
|
|
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)
|
|
4153
|
-
//
|
|
4154
|
-
//
|
|
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
|
}
|