@animalabs/agent-framework 0.28.0 → 0.30.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 +88 -25
- 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 +21 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +47 -7
- 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 +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -3
- package/dist/src/api/server.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/auto-continue.d.ts +2 -0
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +5 -2
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- 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 +846 -67
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4213 -924
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +46 -14
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +143 -53
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/types.d.ts +12 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +34 -8
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +174 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +565 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +118 -15
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +266 -110
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +3 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +3 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +9 -0
- 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/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +20 -20
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +18 -17
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +17 -16
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +8 -26
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +16 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -25
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +187 -50
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +28 -12
- 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 +6 -7
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +6 -7
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.js +8 -6
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +5 -1
- package/dist/src/modules/workspace/answers.d.ts.map +1 -1
- package/dist/src/modules/workspace/answers.js +16 -1
- package/dist/src/modules/workspace/answers.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +6 -4
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +50 -40
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +1 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +66 -36
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +51 -20
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +120 -82
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +133 -37
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +81 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +85 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
- package/dist/src/reasoning-loss-notice.js +5 -3
- package/dist/src/reasoning-loss-notice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +5 -55
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +63 -7
- 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 +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +9 -8
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +382 -11
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +1100 -119
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +101 -5
- 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 +48 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +142 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -11
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +136 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/admission/admit.ts +113 -0
- package/src/agent.ts +52 -7
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/author-presentation.ts +81 -0
- package/src/auto-continue.ts +8 -2
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- 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 +4362 -852
- package/src/gate/event-gate.ts +159 -52
- package/src/gate/index.ts +1 -1
- package/src/gate/types.ts +12 -2
- package/src/heartbeat/heartbeat.ts +48 -9
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +612 -0
- package/src/index.ts +17 -4
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +306 -111
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +3 -1
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-coalescer.ts +19 -0
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +20 -18
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tool-presence.ts +18 -17
- package/src/mcpl/tool-signature.ts +17 -16
- package/src/mcpl/toolset-host.ts +18 -6
- package/src/mcpl/toolsets-module.ts +149 -50
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +29 -12
- package/src/module-registry.ts +6 -7
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +8 -6
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +20 -2
- package/src/modules/workspace/direct-write.ts +4 -3
- package/src/modules/workspace/effects.ts +50 -40
- package/src/modules/workspace/index.ts +58 -34
- package/src/modules/workspace/observe.ts +139 -88
- package/src/modules/workspace/reconcile.ts +159 -36
- package/src/notice-tags.ts +86 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +11 -7
- package/src/stored-injections.ts +7 -0
- package/src/supplied-text.ts +154 -0
- package/src/tool-args.ts +9 -8
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tune-out/coordinator.ts +1252 -123
- package/src/tune-out/tools.ts +152 -11
- package/src/types/agent.ts +51 -9
- package/src/types/framework.ts +146 -0
- package/src/types/module.ts +23 -11
- package/src/types/trace.ts +150 -0
package/src/gate/event-gate.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* inference tokens, not whether the agent should know about the event.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
15
|
+
import { cutToFit, suppliedValue } from '../supplied-text.js';
|
|
14
16
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
17
|
import { basename, dirname, join } from 'node:path';
|
|
16
18
|
import { GateScript } from './gate-script.js';
|
|
@@ -343,7 +345,7 @@ function referenceLine(count: number, channel: string, oldest: number, now: numb
|
|
|
343
345
|
/** A handed batched-wake line's text (WakeLine), as it reads when written at
|
|
344
346
|
* `now`: the gate's words, its message's age counted to then. */
|
|
345
347
|
export function wakeLineText(line: WakeLine, now: number): string {
|
|
346
|
-
return `[
|
|
348
|
+
return `[${NOTICE_TAG.gate}: 1 event matched]\n\n${referenceLine(1, line.shown, line.at, now)}`;
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
/** The event's own stored entry, as the framework is asked about it. */
|
|
@@ -1825,30 +1827,72 @@ function globAtLeastAsBroad(earlier: string | undefined, later: string | undefin
|
|
|
1825
1827
|
return earlier === later;
|
|
1826
1828
|
}
|
|
1827
1829
|
|
|
1830
|
+
/** The event types the gate can see from an MCPL server: its pushes and its
|
|
1831
|
+
* channels' messages. */
|
|
1832
|
+
export const MCPL_GATE_EVENT_TYPES: readonly string[] = ['mcpl:push-event', 'mcpl:channel-incoming'];
|
|
1833
|
+
|
|
1834
|
+
/** The event types a host knows a source sends (EventGate's
|
|
1835
|
+
* `eventTypesFrom`), or undefined when it could send any. */
|
|
1836
|
+
export type EventTypesFrom = (source: string) => readonly string[] | undefined;
|
|
1837
|
+
|
|
1838
|
+
/** The event types `rule` can match: its scope's, narrowed to those its
|
|
1839
|
+
* source is known to send; 'all' when neither says. */
|
|
1840
|
+
function typesSeenBy(rule: GatePolicy, eventTypesFrom?: EventTypesFrom): { types: string[] | 'all'; known: boolean } {
|
|
1841
|
+
const scope = rule.match.scope && rule.match.scope.length > 0 ? rule.match.scope : undefined;
|
|
1842
|
+
const source = rule.match.source;
|
|
1843
|
+
let sent: readonly string[] | undefined;
|
|
1844
|
+
if (eventTypesFrom && source !== undefined && !source.includes('*')) {
|
|
1845
|
+
try {
|
|
1846
|
+
sent = eventTypesFrom(source);
|
|
1847
|
+
} catch {
|
|
1848
|
+
sent = undefined;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (scope && sent) return { types: scope.filter((t) => sent!.includes(t)), known: true };
|
|
1852
|
+
if (scope) return { types: scope, known: true };
|
|
1853
|
+
if (sent) return { types: [...sent], known: true };
|
|
1854
|
+
return { types: 'all', known: false };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1828
1857
|
/**
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1858
|
+
* Whether `earlier` provably matches events `later` would match, so that
|
|
1859
|
+
* `later` doesn't see them when `earlier` sits before it and consumes its
|
|
1860
|
+
* matches: the event types concerned, whether that's every event `later`
|
|
1861
|
+
* would match, and whether `later` is known to see those types. Returns null
|
|
1862
|
+
* when it can't be proven (which is not proof of safety — the lint is
|
|
1863
|
+
* deliberately one-sided so every note it emits is true).
|
|
1834
1864
|
*/
|
|
1835
|
-
function subsumedEventTypes(
|
|
1865
|
+
function subsumedEventTypes(
|
|
1866
|
+
earlier: GatePolicy,
|
|
1867
|
+
later: GatePolicy,
|
|
1868
|
+
eventTypesFrom?: EventTypesFrom,
|
|
1869
|
+
): Omit<ShadowWarning, 'earlier' | 'later'> | null {
|
|
1836
1870
|
const e = earlier.match;
|
|
1837
1871
|
const l = later.match;
|
|
1838
1872
|
|
|
1839
|
-
//
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1873
|
+
// Event types: which of those `later` can match does `earlier` cover?
|
|
1874
|
+
const seen = typesSeenBy(later, eventTypesFrom);
|
|
1875
|
+
const earlierTypes = e.scope && e.scope.length > 0 ? e.scope : 'all';
|
|
1876
|
+
let eventTypes: string[] | 'all';
|
|
1877
|
+
let whole: boolean;
|
|
1878
|
+
let known = seen.known;
|
|
1879
|
+
if (earlierTypes === 'all') {
|
|
1880
|
+
eventTypes = seen.types;
|
|
1881
|
+
whole = true;
|
|
1882
|
+
} else if (seen.types === 'all') {
|
|
1883
|
+
// `later` matches every type; `earlier` only consumes its own scope, so
|
|
1884
|
+
// the overlap is partial — exactly those types, which may never reach
|
|
1885
|
+
// `later`.
|
|
1886
|
+
eventTypes = earlierTypes;
|
|
1887
|
+
whole = false;
|
|
1847
1888
|
} else {
|
|
1848
|
-
const overlap =
|
|
1889
|
+
const overlap = seen.types.filter((t) => earlierTypes.includes(t));
|
|
1849
1890
|
if (overlap.length === 0) return null;
|
|
1850
|
-
|
|
1891
|
+
eventTypes = overlap;
|
|
1892
|
+
whole = overlap.length === seen.types.length;
|
|
1893
|
+
known = true;
|
|
1851
1894
|
}
|
|
1895
|
+
if (Array.isArray(eventTypes) && eventTypes.length === 0) return null;
|
|
1852
1896
|
|
|
1853
1897
|
// Every remaining discriminator on `earlier` must be at least as broad as
|
|
1854
1898
|
// `later`'s, otherwise some `later` events slip past `earlier`.
|
|
@@ -1876,38 +1920,55 @@ function subsumedEventTypes(earlier: GatePolicy, later: GatePolicy): string[] |
|
|
|
1876
1920
|
if (e.tagsAll && !subsetOf(e.tagsAll, l.tagsAll ?? [])) return null;
|
|
1877
1921
|
if (e.tagsNone && !subsetOf(e.tagsNone, l.tagsNone ?? [])) return null;
|
|
1878
1922
|
|
|
1879
|
-
return
|
|
1923
|
+
return { eventTypes, whole, known };
|
|
1880
1924
|
}
|
|
1881
1925
|
|
|
1882
1926
|
/**
|
|
1883
|
-
* Find rules
|
|
1927
|
+
* Find rules an earlier, broader rule keeps from applying (first match wins),
|
|
1928
|
+
* in whole or for some event types.
|
|
1884
1929
|
* The 2026-08-08 Mythos incident is the canonical catch: a prepended
|
|
1885
1930
|
* `{scope:[mcpl:channel-incoming]}` sampler silently made the
|
|
1886
1931
|
* direct-address and mention rules dead for the whole incoming lane.
|
|
1887
1932
|
* `passthrough` observers shadow nothing — non-firing matches fall through.
|
|
1933
|
+
* `eventTypesFrom` leaves out types a rule's source is known never to send:
|
|
1934
|
+
* a rule for an MCPL server's events isn't told about another rule that
|
|
1935
|
+
* takes every `external-message`.
|
|
1888
1936
|
*/
|
|
1889
|
-
export function findShadowedPolicies(policies: GatePolicy[]): ShadowWarning[] {
|
|
1937
|
+
export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: EventTypesFrom): ShadowWarning[] {
|
|
1890
1938
|
const out: ShadowWarning[] = [];
|
|
1891
1939
|
for (let i = 0; i < policies.length; i++) {
|
|
1892
1940
|
const earlier = policies[i];
|
|
1893
1941
|
if (earlier.passthrough) continue;
|
|
1894
1942
|
for (let j = i + 1; j < policies.length; j++) {
|
|
1895
|
-
const
|
|
1896
|
-
if (
|
|
1897
|
-
out.push({ earlier: earlier.name, later: policies[j].name,
|
|
1943
|
+
const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
|
|
1944
|
+
if (found) {
|
|
1945
|
+
out.push({ earlier: earlier.name, later: policies[j].name, ...found });
|
|
1898
1946
|
}
|
|
1899
1947
|
}
|
|
1900
1948
|
}
|
|
1901
1949
|
return out;
|
|
1902
1950
|
}
|
|
1903
1951
|
|
|
1904
|
-
/**
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1952
|
+
/** A ShadowWarning in words, used in tool results, traces, and gate_status
|
|
1953
|
+
* alike so the same note reads the same way everywhere. It says which of
|
|
1954
|
+
* three it is (the rule never applies; it misses some of its events; it
|
|
1955
|
+
* may not be meant for those events at all) and what to do. */
|
|
1907
1956
|
export function formatShadowWarning(w: ShadowWarning): string {
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
1957
|
+
const later = `"${w.later}"`;
|
|
1958
|
+
const earlier = `"${w.earlier}"`;
|
|
1959
|
+
const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: "${w.earlier}")`;
|
|
1960
|
+
if (w.whole) {
|
|
1961
|
+
return `${later} never applies: ${earlier} comes first and matches every event ${later} would. `
|
|
1962
|
+
+ `If ${later} should apply, ${move}, or narrow ${earlier}.`;
|
|
1963
|
+
}
|
|
1964
|
+
const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.join(', ');
|
|
1965
|
+
if (w.known) {
|
|
1966
|
+
return `${earlier} comes first and takes ${later}'s ${types} events, so ${later} applies only to its others. `
|
|
1967
|
+
+ `If it should apply to those too, ${move}.`;
|
|
1968
|
+
}
|
|
1969
|
+
return `${earlier} comes first and takes every ${types} event, so ${later} sees none. `
|
|
1970
|
+
+ `Nothing to do unless ${later} is meant for ${types} events; then ${move}. `
|
|
1971
|
+
+ `A scope on ${later} naming the event types it's for clears this note.`;
|
|
1911
1972
|
}
|
|
1912
1973
|
|
|
1913
1974
|
/** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
|
|
@@ -2035,10 +2096,12 @@ export class EventGate {
|
|
|
2035
2096
|
// Per-policy stats
|
|
2036
2097
|
private stats = new Map<string, PolicyStats>();
|
|
2037
2098
|
|
|
2038
|
-
//
|
|
2039
|
-
// config change
|
|
2040
|
-
|
|
2099
|
+
// The shadow lint's last findings (see refreshShadowWarnings), recomputed
|
|
2100
|
+
// on every config change and fingerprinted so an unchanged set doesn't
|
|
2101
|
+
// re-trace.
|
|
2041
2102
|
private shadowWarningsFingerprint = '[]';
|
|
2103
|
+
/** See the constructor's `eventTypesFrom`. */
|
|
2104
|
+
private eventTypesFromFn: EventTypesFrom | undefined;
|
|
2042
2105
|
|
|
2043
2106
|
// Observability counters for events that didn't match any policy
|
|
2044
2107
|
private totalEvaluations = 0;
|
|
@@ -2093,6 +2156,8 @@ export class EventGate {
|
|
|
2093
2156
|
) => unknown;
|
|
2094
2157
|
private requestInferenceFn: (agentName: string, reason: string, source: string, provenance?: WakeProvenance, after?: WritePlacement) => void;
|
|
2095
2158
|
private getAgentNamesFn: () => string[];
|
|
2159
|
+
/** Delivery-time re-check: is this channel held from the recipients now? */
|
|
2160
|
+
private isHeldFn: (serverId: string | undefined, channelId: string | undefined) => boolean;
|
|
2096
2161
|
private resolveRouteChannelFn: (info: GateEventInfo) => string | undefined;
|
|
2097
2162
|
private channelShownFn?: (channel: { serverId: string; channelId: string }) => string;
|
|
2098
2163
|
private isNewestEntryFn?: (entry: GateEntryRef) => boolean;
|
|
@@ -2117,6 +2182,13 @@ export class EventGate {
|
|
|
2117
2182
|
* message is answered. */
|
|
2118
2183
|
requestInference: (agentName: string, reason: string, source: string, provenance?: WakeProvenance, after?: WritePlacement) => void;
|
|
2119
2184
|
getAgentNames: () => string[];
|
|
2185
|
+
/**
|
|
2186
|
+
* Whether a channel is held from the recipients (a tune-out epoch in
|
|
2187
|
+
* force). A held event never enters a batch — admission runs before the
|
|
2188
|
+
* gate — but a hold can begin after an event was batched; at delivery
|
|
2189
|
+
* the batch is re-checked and held events are dropped (#232 H3).
|
|
2190
|
+
*/
|
|
2191
|
+
isHeld?: (serverId: string | undefined, channelId: string | undefined) => boolean;
|
|
2120
2192
|
/** Optional clock — defaults to Date.now. Tests inject for deterministic time. */
|
|
2121
2193
|
now?: () => number;
|
|
2122
2194
|
/** Per-event timeout (ms) for the optional gate.js script. Default 50. */
|
|
@@ -2153,6 +2225,11 @@ export class EventGate {
|
|
|
2153
2225
|
* (WakeProvenance.line), for a fresh start of the wake's turn to decide
|
|
2154
2226
|
* (WakeLine). Absent: every batched wake writes its line, as before. */
|
|
2155
2227
|
isNewestEntry?: (entry: GateEntryRef) => boolean;
|
|
2228
|
+
/** The event types the gate can see from a source, when the host knows
|
|
2229
|
+
* them (an MCPL server's id: MCPL_GATE_EVENT_TYPES), else undefined.
|
|
2230
|
+
* The rule-order notes (findShadowedPolicies) leave out types a rule's
|
|
2231
|
+
* source never sends. Absent: any source can send any type. */
|
|
2232
|
+
eventTypesFrom?: EventTypesFrom;
|
|
2156
2233
|
}) {
|
|
2157
2234
|
this.configPath = opts.configPath;
|
|
2158
2235
|
this.privilegedUsersPath = opts.privilegedUsersPath;
|
|
@@ -2160,9 +2237,11 @@ export class EventGate {
|
|
|
2160
2237
|
this.addMessageFn = opts.addMessage;
|
|
2161
2238
|
this.requestInferenceFn = opts.requestInference;
|
|
2162
2239
|
this.getAgentNamesFn = opts.getAgentNames;
|
|
2240
|
+
this.isHeldFn = opts.isHeld ?? (() => false);
|
|
2163
2241
|
this.resolveRouteChannelFn = opts.resolveRouteChannel ?? defaultRouteChannel;
|
|
2164
2242
|
this.channelShownFn = opts.channelShown;
|
|
2165
2243
|
this.isNewestEntryFn = opts.isNewestEntry;
|
|
2244
|
+
this.eventTypesFromFn = opts.eventTypesFrom;
|
|
2166
2245
|
this.now = opts.now ?? (() => Date.now());
|
|
2167
2246
|
this.judgeFn = opts.judge;
|
|
2168
2247
|
this.changesJudgedContextFn = opts.changesJudgedContext;
|
|
@@ -3448,22 +3527,32 @@ export class EventGate {
|
|
|
3448
3527
|
/** Current shadow-lint findings for the live policy order. Fresh compute —
|
|
3449
3528
|
* cheap (policy lists are tens of entries at most). */
|
|
3450
3529
|
lintShadows(): ShadowWarning[] {
|
|
3451
|
-
return findShadowedPolicies(this.config.policies);
|
|
3530
|
+
return findShadowedPolicies(this.config.policies, this.eventTypesFromFn);
|
|
3452
3531
|
}
|
|
3453
3532
|
|
|
3454
3533
|
/**
|
|
3455
|
-
*
|
|
3456
|
-
*
|
|
3457
|
-
*
|
|
3458
|
-
|
|
3534
|
+
* The sources the host knows changed (eventTypesFrom answers differently),
|
|
3535
|
+
* as when an MCPL server connects or is removed: the rule-order notes are
|
|
3536
|
+
* made again, and traced if they changed, as a change to the rules would.
|
|
3537
|
+
*/
|
|
3538
|
+
sourcesChanged(): void {
|
|
3539
|
+
this.refreshShadowWarnings('sources-changed');
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
/**
|
|
3543
|
+
* Recompute shadow warnings and, when the set changed, emit a
|
|
3544
|
+
* `gate:shadow-warnings` trace so the hazard lands in ops observability at
|
|
3545
|
+
* the moment the order changed — not at the next missed DM — and, with an
|
|
3546
|
+
* empty list, the moment the last of them went. Called from writeConfig,
|
|
3547
|
+
* reloadIfChanged and sourcesChanged.
|
|
3459
3548
|
*/
|
|
3460
3549
|
private refreshShadowWarnings(reason: string): void {
|
|
3461
3550
|
const warnings = this.lintShadows();
|
|
3462
3551
|
const fingerprint = JSON.stringify(warnings);
|
|
3463
3552
|
if (fingerprint === this.shadowWarningsFingerprint) return;
|
|
3553
|
+
const hadSome = this.shadowWarningsFingerprint !== '[]';
|
|
3464
3554
|
this.shadowWarningsFingerprint = fingerprint;
|
|
3465
|
-
|
|
3466
|
-
if (warnings.length > 0) {
|
|
3555
|
+
if (warnings.length > 0 || hadSome) {
|
|
3467
3556
|
this.emitTrace({
|
|
3468
3557
|
type: 'gate:shadow-warnings',
|
|
3469
3558
|
reason,
|
|
@@ -3608,7 +3697,23 @@ export class EventGate {
|
|
|
3608
3697
|
// Event delivery
|
|
3609
3698
|
// =========================================================================
|
|
3610
3699
|
|
|
3611
|
-
private deliverEvents(
|
|
3700
|
+
private deliverEvents(batched: PendingEvent[]): void {
|
|
3701
|
+
// Delivery-time re-check: a channel tuned out after its events were
|
|
3702
|
+
// batched is held now, and "already shown above at arrival" would be
|
|
3703
|
+
// untrue of them. They are dropped; the hold's own reader has them.
|
|
3704
|
+
const heldAt = (e: PendingEvent): string | undefined => e.routeChannelId ?? e.channelId;
|
|
3705
|
+
const events = batched.filter((e) => {
|
|
3706
|
+
const channel = heldAt(e);
|
|
3707
|
+
return !(channel && this.isHeldFn(e.serverId, channel));
|
|
3708
|
+
});
|
|
3709
|
+
if (events.length < batched.length) {
|
|
3710
|
+
this.emitTrace({
|
|
3711
|
+
type: 'gate:held-dropped',
|
|
3712
|
+
dropped: batched.length - events.length,
|
|
3713
|
+
channels: [...new Set(batched.filter((e) => !events.includes(e)).map(heldAt))],
|
|
3714
|
+
timestamp: this.now(),
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3612
3717
|
if (events.length === 0) return;
|
|
3613
3718
|
|
|
3614
3719
|
const policyNames = [...new Set(events.map(e => e.policyName))];
|
|
@@ -3621,7 +3726,7 @@ export class EventGate {
|
|
|
3621
3726
|
const quoted = events.filter(e => !e.inContext);
|
|
3622
3727
|
const referenced = events.filter(e => e.inContext);
|
|
3623
3728
|
|
|
3624
|
-
const lines: string[] = quoted.map(e => `- [${e.policyName}] (${e.eventType}): ${e.content}`);
|
|
3729
|
+
const lines: string[] = quoted.map(e => `- [${e.policyName}] (${e.eventType}): ${suppliedValue(e.content)}`);
|
|
3625
3730
|
// How each referenced channel is named in the line, in order.
|
|
3626
3731
|
const shown: string[] = [];
|
|
3627
3732
|
if (referenced.length > 0) {
|
|
@@ -3640,7 +3745,7 @@ export class EventGate {
|
|
|
3640
3745
|
? '(unknown channel)'
|
|
3641
3746
|
: e.routeChannelId !== undefined && e.serverId !== undefined && this.channelShownFn
|
|
3642
3747
|
? this.channelShownFn({ serverId: e.serverId, channelId: e.routeChannelId })
|
|
3643
|
-
: id;
|
|
3748
|
+
: suppliedValue(id);
|
|
3644
3749
|
const key = JSON.stringify([e.serverId ?? null, id ?? null]);
|
|
3645
3750
|
const entry = byChannel.get(key) ?? { name, count: 0, oldest: e.timestamp, label: e.channelLabel };
|
|
3646
3751
|
entry.count += 1;
|
|
@@ -3653,7 +3758,7 @@ export class EventGate {
|
|
|
3653
3758
|
// agent can still address tools that want the composite id. A name
|
|
3654
3759
|
// may carry its own `#`, so one is added only where the name lacks
|
|
3655
3760
|
// it, as at the other places that add a `#`.
|
|
3656
|
-
const channel = label ? `${label.startsWith('#') ?
|
|
3761
|
+
const channel = label ? `${label.startsWith('#') ? '' : '#'}${suppliedValue(label)} (${name})` : name;
|
|
3657
3762
|
shown.push(channel);
|
|
3658
3763
|
lines.push(referenceLine(count, channel, oldest, Date.now()));
|
|
3659
3764
|
}
|
|
@@ -3666,7 +3771,7 @@ export class EventGate {
|
|
|
3666
3771
|
// gate hands the line to the wake (WakeProvenance.line) and a fresh start
|
|
3667
3772
|
// of the wake's turn decides it (WakeLine). Several messages, one with
|
|
3668
3773
|
// anything after it, or content the line quotes, keep the line now.
|
|
3669
|
-
const text = `[
|
|
3774
|
+
const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
|
|
3670
3775
|
const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
|
|
3671
3776
|
let line: WakeLine | undefined;
|
|
3672
3777
|
if (only && this.newestEntry(only)) {
|
|
@@ -3911,7 +4016,7 @@ export class EventGate {
|
|
|
3911
4016
|
record.last = {
|
|
3912
4017
|
at: Date.now(),
|
|
3913
4018
|
verdict: outcome,
|
|
3914
|
-
reason:
|
|
4019
|
+
reason: cutToFit(suppliedValue(reason), 300),
|
|
3915
4020
|
eventCount: state.events.length,
|
|
3916
4021
|
durationMs,
|
|
3917
4022
|
...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
|
|
@@ -3919,7 +4024,7 @@ export class EventGate {
|
|
|
3919
4024
|
if (this.config.policies.some((p) => p.name === policyName)) this.judgeRecords.set(policyName, record);
|
|
3920
4025
|
if (failed) {
|
|
3921
4026
|
this.judgeFailStreak++;
|
|
3922
|
-
this.judgeLastFailure =
|
|
4027
|
+
this.judgeLastFailure = cutToFit(suppliedValue(reason), 200);
|
|
3923
4028
|
} else {
|
|
3924
4029
|
this.judgeFailStreak = 0;
|
|
3925
4030
|
}
|
|
@@ -4190,10 +4295,9 @@ export class EventGate {
|
|
|
4190
4295
|
}
|
|
4191
4296
|
|
|
4192
4297
|
/**
|
|
4193
|
-
* `agentName` is gone, as
|
|
4194
|
-
*
|
|
4195
|
-
*
|
|
4196
|
-
* framework refuses their `sleep`).
|
|
4298
|
+
* `agentName` is gone, as an agent's end removes one: its pending
|
|
4299
|
+
* self-wake is cancelled, and a sleep it set ends, rather than go on
|
|
4300
|
+
* quieting every agent and then wake a name that no longer exists.
|
|
4197
4301
|
*/
|
|
4198
4302
|
agentGone(agentName: string): void {
|
|
4199
4303
|
const selfWake = this.selfWakeTimers.get(agentName);
|
|
@@ -4201,6 +4305,7 @@ export class EventGate {
|
|
|
4201
4305
|
clearTimeout(selfWake);
|
|
4202
4306
|
this.selfWakeTimers.delete(agentName);
|
|
4203
4307
|
}
|
|
4308
|
+
if (this.sleepAgent === agentName) this.clearSleep();
|
|
4204
4309
|
}
|
|
4205
4310
|
|
|
4206
4311
|
// =========================================================================
|
|
@@ -4246,7 +4351,7 @@ export class EventGate {
|
|
|
4246
4351
|
const placement: WritePlacement = {};
|
|
4247
4352
|
this.addMessageFn('user', [{
|
|
4248
4353
|
type: 'text',
|
|
4249
|
-
text: `[
|
|
4354
|
+
text: `[${NOTICE_TAG.selfWake}] your ${source} timer (${Math.round(ms / 1000)}s) elapsed — now ${nowIso}`,
|
|
4250
4355
|
}], { source: 'gate:self-wake' }, agentName, placement);
|
|
4251
4356
|
this.requestInferenceFn(
|
|
4252
4357
|
agentName,
|
|
@@ -4497,7 +4602,9 @@ export class EventGate {
|
|
|
4497
4602
|
this.judgeUnwiredNotice(), this.judgeFailingNotice(),
|
|
4498
4603
|
]
|
|
4499
4604
|
.filter((e): e is string => e !== null),
|
|
4500
|
-
|
|
4605
|
+
// Fresh, not the cache: the sources the host knows (eventTypesFrom)
|
|
4606
|
+
// change as servers connect, with no change to the rules.
|
|
4607
|
+
shadowWarnings: this.lintShadows().map(formatShadowWarning),
|
|
4501
4608
|
totalEvaluations: this.totalEvaluations,
|
|
4502
4609
|
defaultDecisions: {
|
|
4503
4610
|
triggered: this.defaultTriggered,
|
package/src/gate/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { EventGate, wakeProvenance, wakeLineText, type WakeProvenance, type WritePlacement } from './event-gate.js';
|
|
1
|
+
export { EventGate, MCPL_GATE_EVENT_TYPES, wakeProvenance, wakeLineText, type EventTypesFrom, type WakeProvenance, type WritePlacement } from './event-gate.js';
|
|
2
2
|
export type {
|
|
3
3
|
GateConfig,
|
|
4
4
|
GateOptions,
|
package/src/gate/types.ts
CHANGED
|
@@ -359,10 +359,20 @@ export interface GateDecision {
|
|
|
359
359
|
export interface ShadowWarning {
|
|
360
360
|
/** Name of the earlier (consuming) policy. */
|
|
361
361
|
earlier: string;
|
|
362
|
-
/** Name of the later policy it
|
|
362
|
+
/** Name of the later policy whose events it takes. */
|
|
363
363
|
later: string;
|
|
364
|
-
/** Event types affected
|
|
364
|
+
/** Event types affected: those of `later`'s (its scope's, narrowed to the
|
|
365
|
+
* ones its source is known to send) that `earlier` covers. 'all' only when
|
|
366
|
+
* neither rule has a scope and `later`'s source could send any type. */
|
|
365
367
|
eventTypes: string[] | 'all';
|
|
368
|
+
/** Whether `earlier` takes every event `later` would match, so `later`
|
|
369
|
+
* never applies. */
|
|
370
|
+
whole: boolean;
|
|
371
|
+
/** Whether `later` is known to see these types: its scope names them, or
|
|
372
|
+
* its source is one whose event types the host knows (an MCPL server's).
|
|
373
|
+
* False when `later` names no scope and its source could send anything,
|
|
374
|
+
* so the types may never reach it. */
|
|
375
|
+
known: boolean;
|
|
366
376
|
}
|
|
367
377
|
|
|
368
378
|
/** One row of the dry-run probe table (see EventGate.probeTable). */
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* missed.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
31
32
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
32
33
|
import { dirname } from 'node:path';
|
|
33
34
|
import { checkToolInput, shownValue } from '../gate/event-gate.js';
|
|
@@ -81,7 +82,7 @@ export const DEFAULT_HEARTBEAT: Readonly<HeartbeatSettings> = {
|
|
|
81
82
|
intervalSeconds: 4 * 60 * 60,
|
|
82
83
|
paused: false,
|
|
83
84
|
deliveryMode: 'message',
|
|
84
|
-
message:
|
|
85
|
+
message: `[${NOTICE_TAG.heartbeat}] Periodic self-check-in. Review anything pending — reminders, schedules, follow-ups — and act if needed; otherwise a brief note to yourself is fine.`,
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
/** A scheduled reminder. `everySeconds` null: one-off, dropped once it fires. */
|
|
@@ -112,6 +113,17 @@ export interface HeartbeatOptions {
|
|
|
112
113
|
importFrom?: { configPath?: string; remindersPath?: string };
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/** One of the heartbeat's scheduled wakes: its next tick or a pending reminder. */
|
|
117
|
+
export interface HeartbeatScheduledWake extends UpcomingWake {
|
|
118
|
+
kind: 'heartbeat' | 'reminder';
|
|
119
|
+
at: number;
|
|
120
|
+
/** Whether it's a silent tick, delivered as one (HeartbeatWake's `silent`). */
|
|
121
|
+
silent: boolean;
|
|
122
|
+
/** The text it brings, as of its due time: so the gate can be asked about
|
|
123
|
+
* the event it will judge. */
|
|
124
|
+
text: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/** A wake the heartbeat asks the framework to deliver. */
|
|
116
128
|
export type HeartbeatWake =
|
|
117
129
|
| { kind: 'heartbeat'; reason: 'schedule' | 'manual'; silent: boolean; text: string; eventId: string }
|
|
@@ -223,11 +235,16 @@ function callAt(dueAt: number, now: () => number, onDue: () => void): () => void
|
|
|
223
235
|
return () => clearTimeout(timer);
|
|
224
236
|
}
|
|
225
237
|
|
|
238
|
+
/** The text a tick brings: the time, then its message. */
|
|
239
|
+
function tickText(now: number, message: string, timeZone: string): string {
|
|
240
|
+
return `[${NOTICE_TAG.currentTime}: ${formatHeartbeatTime(now, timeZone)}] ${message}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
226
243
|
/** The text a reminder brings: a header naming it, then its message. */
|
|
227
244
|
export function reminderText(rem: Reminder, now: number, timeZone: string): string {
|
|
228
245
|
let schedule = rem.everySeconds !== null ? `, repeats every ${rem.everySeconds}s` : `, set ${formatHeartbeatTime(rem.createdAt, timeZone)}`;
|
|
229
246
|
if (now - rem.fireAt >= LATE_MS) schedule += `, due ${formatHeartbeatTime(rem.fireAt, timeZone)}`;
|
|
230
|
-
return `[reminder ${rem.id}${schedule} · current time: ${formatHeartbeatTime(now, timeZone)}] ${rem.message}`;
|
|
247
|
+
return `[${NOTICE_TAG.reminder} ${rem.id}${schedule} · current time: ${formatHeartbeatTime(now, timeZone)}] ${rem.message}`;
|
|
231
248
|
}
|
|
232
249
|
|
|
233
250
|
/** A length of time as the agent is shown it. */
|
|
@@ -318,24 +335,46 @@ export class Heartbeat {
|
|
|
318
335
|
|
|
319
336
|
/** What the heartbeat will wake the agent with, soonest first, for the reply judge (UpcomingWake). */
|
|
320
337
|
upcomingWakes(): UpcomingWake[] {
|
|
321
|
-
|
|
338
|
+
return this.scheduledWakes().map(({ at, certain, what }) => ({ at, certain, what }));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The heartbeat's next tick and its pending reminders, soonest first, each
|
|
343
|
+
* with its kind and the event it will bring, so the gate can be asked about
|
|
344
|
+
* it. `whose` words each for the reply judge ("its heartbeat", as
|
|
345
|
+
* upcomingWakes) or for the agent ("your heartbeat", as skip_reply's
|
|
346
|
+
* confirmation names one due before its self-wake).
|
|
347
|
+
*/
|
|
348
|
+
scheduledWakes(whose: 'its' | 'your' = 'its'): HeartbeatScheduledWake[] {
|
|
349
|
+
const wakes: HeartbeatScheduledWake[] = [];
|
|
322
350
|
const s = this.state;
|
|
323
351
|
if (!s.settings.paused && s.nextFireAt !== null) {
|
|
352
|
+
const at = Math.max(s.nextFireAt, this.notBefore);
|
|
353
|
+
const silent = s.settings.deliveryMode === 'silent';
|
|
324
354
|
wakes.push({
|
|
325
|
-
|
|
355
|
+
kind: 'heartbeat',
|
|
356
|
+
at,
|
|
326
357
|
certain: true,
|
|
327
|
-
what:
|
|
358
|
+
what: `${whose} heartbeat (${silent ? 'a silent tick' : 'a check-in prompt'}, every ${shownLength(s.settings.intervalSeconds)})`,
|
|
359
|
+
silent,
|
|
360
|
+
// A host's interval can put the tick past the last time a date
|
|
361
|
+
// shows (readSettings has no ceiling), and a time can't be shown there.
|
|
362
|
+
text: canHoldTime(at) ? tickText(at, s.settings.message, this.timeZone) : s.settings.message,
|
|
328
363
|
});
|
|
329
364
|
}
|
|
330
365
|
for (const r of s.reminders) {
|
|
331
366
|
const message = r.message.length > 80 ? `${r.message.slice(0, 77)}...` : r.message;
|
|
367
|
+
const at = Math.max(dueAt(r), this.notBefore);
|
|
332
368
|
wakes.push({
|
|
333
|
-
|
|
369
|
+
kind: 'reminder',
|
|
370
|
+
at,
|
|
334
371
|
certain: true,
|
|
335
|
-
what:
|
|
372
|
+
what: `${whose} reminder ${JSON.stringify(message)}${r.everySeconds !== null ? ` (repeats every ${shownLength(r.everySeconds)})` : ''}`,
|
|
373
|
+
silent: false,
|
|
374
|
+
text: reminderText(r, at, this.timeZone),
|
|
336
375
|
});
|
|
337
376
|
}
|
|
338
|
-
return wakes.sort((a, b) =>
|
|
377
|
+
return wakes.sort((a, b) => a.at - b.at);
|
|
339
378
|
}
|
|
340
379
|
|
|
341
380
|
/**
|
|
@@ -549,7 +588,7 @@ export class Heartbeat {
|
|
|
549
588
|
kind: 'heartbeat',
|
|
550
589
|
reason,
|
|
551
590
|
silent,
|
|
552
|
-
text:
|
|
591
|
+
text: tickText(now, s.message, this.timeZone),
|
|
553
592
|
eventId: `heartbeat_${now}_${Math.random().toString(36).slice(2, 8)}`,
|
|
554
593
|
});
|
|
555
594
|
log(`fired heartbeat (${reason}${silent ? ', silent' : ''})`);
|
package/src/held-writes.ts
CHANGED
|
@@ -62,9 +62,9 @@ function valueAt(value: unknown, path: string): unknown {
|
|
|
62
62
|
* A row as a module's context answers it: marked `heldFromResidents` when it
|
|
63
63
|
* carries the stamp a tune-out puts on a message it holds from the residents
|
|
64
64
|
* (`metadata.tuneOut`). Every resident's viewFilter leaves a stamped row out
|
|
65
|
-
* of its compiled view (framework.ts, createAgent), and a tool
|
|
66
|
-
* stores a stamped write without injecting it into the live turn, so
|
|
67
|
-
* marked row is one the store keeps and no resident is shown
|
|
65
|
+
* of its compiled view (framework.ts, createAgent), and a resident's tool
|
|
66
|
+
* boundary stores a stamped write without injecting it into the live turn, so
|
|
67
|
+
* a marked row is one the store keeps and no resident is shown
|
|
68
68
|
* (ModuleMessage.heldFromResidents says where its text can still reach
|
|
69
69
|
* one). Any other row comes back as it is.
|
|
70
70
|
*/
|