@sema-agent/mods-kernel 7.55.0 → 7.57.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/LICENSE +1 -1
- package/README.md +6 -4
- package/dist/dispatch.d.ts +7 -2
- package/dist/dispatch.js +140 -69
- package/dist/events.d.ts +12 -2
- package/dist/events.js +66 -42
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/receive.d.ts +8 -1
- package/dist/receive.js +290 -17
- package/dist/registration.d.ts +24 -0
- package/dist/registration.js +6 -7
- package/dist/site-rules.d.ts +15 -4
- package/dist/site-rules.js +24 -42
- package/package.json +3 -2
package/LICENSE
CHANGED
|
@@ -5,7 +5,7 @@ Parameters
|
|
|
5
5
|
Licensor: clay (github.com/clayboby)
|
|
6
6
|
Licensed Work: @sema-agent/core (sema-core) and the satellite packages
|
|
7
7
|
published from this repository (currently
|
|
8
|
-
@sema-agent/mods-kernel)
|
|
8
|
+
@sema-agent/mods-kernel, @sema-agent/primitives and @sema-agent/core-stores-sql)
|
|
9
9
|
(formerly published as @sema-ai/core)
|
|
10
10
|
The Licensed Work is (c) 2026 clay.
|
|
11
11
|
Additional Use Grant: You may make production use of the Licensed Work for
|
package/README.md
CHANGED
|
@@ -26,7 +26,8 @@ Two copies of the kernel interoperate on plain objects (no `instanceof`, no modu
|
|
|
26
26
|
one copy beside another compares `KERNEL_CONTRACT_VERSION` for equality. Nothing in the kernel or the engine reads it,
|
|
27
27
|
so two copies that differ are told apart only by a host that compares them. The number is an integer independent of the
|
|
28
28
|
package version; it moves only when the chain semantics, the registration form, the event closed set (names and
|
|
29
|
-
`side`) or the failure columns (failure, abort, pinned keys
|
|
29
|
+
`side`) or the failure columns (failure, abort, pinned keys, an event's own budget ceiling, whether `$` may call it)
|
|
30
|
+
change — once per release, however many of them change in it.
|
|
30
31
|
|
|
31
32
|
## Which side raises an event
|
|
32
33
|
|
|
@@ -51,12 +52,13 @@ contract; a removed or re-kinded export is named as breaking in the `@sema-agent
|
|
|
51
52
|
| Group | Names |
|
|
52
53
|
| --- | --- |
|
|
53
54
|
| Contract and closed sets | `KERNEL_CONTRACT_VERSION` `MOD_TIERS` `MOD_SEATS` `MOD_EVENT_SIDES` `MOD_FAILURE_ANSWERS` `MOD_ABORT_POSTURES` `NEXT_TO_TABLE` `SEAT_TIER_TABLE` |
|
|
54
|
-
| Event table and readers | `MOD_EVENT_TABLE` `isModEventName` `expandModPattern` `stationOn` `readOnSide` `nextToRefusal` |
|
|
55
|
+
| Event table and readers | `MOD_EVENT_TABLE` `isModEventName` `expandModPattern` `expandOnSide` `stationOn` `readOnSide` `failureRefuses` `nextToRefusal` |
|
|
55
56
|
| Event table types | `ModTier` `ModRegistrationTier` `ModSeat` `ModEventSide` `ModBusSide` `ModFailureAnswer` `ModAbortPosture` `ModStation` `ModEventRow` `ModEventName` |
|
|
56
57
|
| Budgets | `DEFAULT_MOD_BUDGET_MS` `CATCH_BUDGET_MS` `RUNAWAY_AFTER_MS` `POST_ABORT_GRACE_MS` `MAX_TIMER_MS` `resolveModBudgetMs` `ModBudgetReading` |
|
|
57
|
-
| Failure causes and site rules | `MOD_FAILURE_CAUSES` `ModFailureCause` `ModEventPayload` `ModAnswerCheck` `ModAnswerer` `ModMatcher` `ModSkip` |
|
|
58
|
+
| Failure causes and site rules | `MOD_FAILURE_CAUSES` `ModFailureCause` `ModEventPayload` `ModAnswerCheck` `ModAnswerRanks` `ModDispatchRun` `ModAnswerer` `ModMatcher` `ModSkip` |
|
|
58
59
|
| Registration | `ENGINE_ORIGIN` `MODS_INVALID_CODE` `MODS_REGISTRATION_REFUSED_CODE` `MOD_TRACE_OUTCOMES` `ModTraceOutcome` `ModUses` `ModOrigin` `ModPluginFacts` `ModDollarBase` `ModTraceEntry` `ModCaughtError` `ModNext` `ModHandler` `ModHandlerOptions` `On` `ModRegistration` `ModAdmissionRecord` `ModSeats` |
|
|
59
|
-
|
|
|
60
|
+
| Typed event maps | `ModEventPair` `ModEventMap` `ModTypedNext` `ModTypedHandler` `MappedOn` |
|
|
61
|
+
| Admission and the bus | `admitMods` `ModFrame` `ModBottomContext` `ModMethodBottom` `ModMethodSeat` `ModMethodBottoms` `ModCallEntry` `ModCallEntries` `ModDispatchOutcome` `ModDispatchOptions` `ModBus` `ModMountedHandler` `ModBusInput` |
|
|
60
62
|
|
|
61
63
|
## License
|
|
62
64
|
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ModBusSide, type ModEventName, type ModSeat } from "./events.js";
|
|
2
2
|
import { type ModAdmissionRecord, type ModDollarBase, type ModOrigin, type ModSeats } from "./registration.js";
|
|
3
|
-
import { type ModAnswerCheck, type ModEventPayload, type ModFailureCause, type ModMatcher, type ModSkip, type ProxyProbe } from "./site-rules.js";
|
|
3
|
+
import { type ModAnswerCheck, type ModAnswerRanks, type ModEventPayload, type ModFailureCause, type ModMatcher, type ModSkip, type ProxyProbe } from "./site-rules.js";
|
|
4
4
|
export type ModFrame = {
|
|
5
5
|
readonly kind: "dollar";
|
|
6
6
|
readonly plugin: string;
|
|
@@ -20,6 +20,8 @@ export interface ModMethodSeat {
|
|
|
20
20
|
readonly enter?: (args: ModEventPayload) => ModEventPayload;
|
|
21
21
|
}
|
|
22
22
|
export type ModMethodBottoms = Partial<Record<ModEventName, ModMethodSeat>>;
|
|
23
|
+
export type ModCallEntry = (args: ModEventPayload, ctx: ModBottomContext) => unknown;
|
|
24
|
+
export type ModCallEntries = Partial<Record<ModEventName, ModCallEntry>>;
|
|
23
25
|
export type ModDispatchOutcome = {
|
|
24
26
|
readonly kind: "answered";
|
|
25
27
|
readonly answer: unknown;
|
|
@@ -33,6 +35,7 @@ export type ModDispatchOutcome = {
|
|
|
33
35
|
readonly skipped: readonly ModSkip[];
|
|
34
36
|
} | {
|
|
35
37
|
readonly kind: "aborted";
|
|
38
|
+
readonly bottomStarted?: true;
|
|
36
39
|
};
|
|
37
40
|
export interface ModParticipant {
|
|
38
41
|
readonly plugin: string;
|
|
@@ -45,6 +48,7 @@ export interface ModDispatchOptions {
|
|
|
45
48
|
readonly frames?: readonly ModFrame[];
|
|
46
49
|
readonly origin?: ModOrigin;
|
|
47
50
|
readonly checkAnswer?: ModAnswerCheck;
|
|
51
|
+
readonly ranks?: ModAnswerRanks;
|
|
48
52
|
readonly participant?: (p: ModParticipant) => boolean;
|
|
49
53
|
}
|
|
50
54
|
export interface ModMountedHandler {
|
|
@@ -59,12 +63,13 @@ export interface ModBus {
|
|
|
59
63
|
readonly records: readonly ModAdmissionRecord[];
|
|
60
64
|
mounted(): readonly ModMountedHandler[];
|
|
61
65
|
hasHandlers(event: ModEventName, participant?: (p: ModParticipant) => boolean): boolean;
|
|
62
|
-
dispatch(event: ModEventName, e:
|
|
66
|
+
dispatch(event: ModEventName, e: object, options: ModDispatchOptions): Promise<ModDispatchOutcome>;
|
|
63
67
|
}
|
|
64
68
|
export interface ModBusInput<D extends ModDollarBase = ModDollarBase> {
|
|
65
69
|
readonly side: ModBusSide;
|
|
66
70
|
readonly seats: ModSeats<D>;
|
|
67
71
|
readonly bottoms: ModMethodBottoms;
|
|
72
|
+
readonly entries?: ModCallEntries;
|
|
68
73
|
readonly report: (err: Error, classification?: "observational-hook-verdict-ignored") => void;
|
|
69
74
|
readonly describeThrown: (err: unknown) => string;
|
|
70
75
|
readonly isProxy?: ProxyProbe;
|
package/dist/dispatch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CATCH_BUDGET_MS, createSelfBudget, POST_ABORT_GRACE_MS, RUNAWAY_AFTER_MS, unrefTimer, watchRunaway } from "./budget.js";
|
|
2
|
-
import {
|
|
2
|
+
import { expandOnSide, failureRefuses, isModEventName, MOD_SEATS, nextToRefusal, stationOn } from "./events.js";
|
|
3
3
|
import { ENGINE_ORIGIN, modsInvalid, screenModSeats, } from "./registration.js";
|
|
4
|
-
import { answerUpward, answerViolation, detachedCopy, engineObjectsOnce, handedOn, matcherMatches, matcherViolation, methodAnswerViolation, NEXT_IN_FLIGHT, receiveAnswer, receiveEvent, receivePayload, } from "./site-rules.js";
|
|
4
|
+
import { answerUpward, answerViolation, detachedCopy, engineObjectsOnce, handedOn, matcherMatches, matcherViolation, methodAnswerViolation, NEXT_IN_FLIGHT, receiveAnswer, receiveEvent, receivePayload, sealedObjects, } from "./site-rules.js";
|
|
5
5
|
const TIER_RANK = { prepend: 0, user: 1, append: 2, builtin: 3, core: 4 };
|
|
6
6
|
const bypassed = (floors, tier) => floors.some((f) => TIER_RANK[f.from] < TIER_RANK[tier] && TIER_RANK[tier] < TIER_RANK[f.to]);
|
|
7
7
|
export async function admitMods(input) {
|
|
@@ -10,7 +10,7 @@ export async function admitMods(input) {
|
|
|
10
10
|
const mounted = [];
|
|
11
11
|
const records = [];
|
|
12
12
|
const describe = safeDescribe(input.describeThrown);
|
|
13
|
-
const bus = createBus(input.side, mounted, input.bottoms, report, records, describe, input.isProxy);
|
|
13
|
+
const bus = createBus(input.side, mounted, input.bottoms, input.entries ?? {}, report, records, describe, input.isProxy);
|
|
14
14
|
const admitted = Object.freeze({ admitted: true });
|
|
15
15
|
for (const cand of screened) {
|
|
16
16
|
if (input.signal?.aborted === true)
|
|
@@ -24,14 +24,10 @@ export async function admitMods(input) {
|
|
|
24
24
|
});
|
|
25
25
|
const outcome = await bus.dispatch("plugin.register", e, {
|
|
26
26
|
bottom: () => admitted,
|
|
27
|
+
ranks: ADMISSION_RANKS,
|
|
27
28
|
...(input.signal !== undefined ? { signal: input.signal } : {}),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: refusalOf(a) === undefined
|
|
31
|
-
? "takes next(e) or { refuse: <reason> }"
|
|
32
|
-
: MOD_SEATS.indexOf(by.seat) > MOD_SEATS.findIndex((s) => s === judged["seat"])
|
|
33
|
-
? `a ${by.seat} registration may not refuse a ${String(judged["seat"])} registration`
|
|
34
|
-
: undefined,
|
|
29
|
+
participant: (p) => MOD_SEATS.indexOf(p.seat) <= MOD_SEATS.indexOf(cand.seat),
|
|
30
|
+
checkAnswer: (a) => (a === admitted || refusalOf(a) !== undefined ? undefined : "takes next(e) or { refuse: <reason> }"),
|
|
35
31
|
});
|
|
36
32
|
if (outcome.kind === "aborted")
|
|
37
33
|
throw input.signal?.reason ?? new Error("admission aborted");
|
|
@@ -59,6 +55,7 @@ export async function admitMods(input) {
|
|
|
59
55
|
}
|
|
60
56
|
return bus;
|
|
61
57
|
}
|
|
58
|
+
const ADMISSION_RANKS = Object.freeze({ of: (a) => (refusalOf(a) !== undefined ? 1 : 0), top: 1 });
|
|
62
59
|
function refusalOf(a) {
|
|
63
60
|
if (typeof a !== "object" || a === null || !Object.prototype.hasOwnProperty.call(a, "refuse"))
|
|
64
61
|
return undefined;
|
|
@@ -113,7 +110,7 @@ function mountOne(cand, side, describe) {
|
|
|
113
110
|
const parsed = handlerOptions(opts);
|
|
114
111
|
if (typeof parsed === "string")
|
|
115
112
|
fail(`on("${String(pattern)}"): ${parsed}`);
|
|
116
|
-
const expanded =
|
|
113
|
+
const expanded = expandOnSide(pattern, side);
|
|
117
114
|
if ("refusal" in expanded)
|
|
118
115
|
return void fail(`on(): ${expanded.refusal}`);
|
|
119
116
|
const outside = expanded.names.filter((n) => !cand.eventsBound.has(n));
|
|
@@ -178,16 +175,23 @@ function handlerOptions(opts) {
|
|
|
178
175
|
}
|
|
179
176
|
const HA = "skipped; what is below it ran in its place";
|
|
180
177
|
const MA = "skipped; its last next() run's result stands";
|
|
181
|
-
function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
182
|
-
const
|
|
178
|
+
function createBus(side, mounted, bottoms, entries, report, records, describe, isProxy) {
|
|
179
|
+
const reaches = new Map();
|
|
183
180
|
for (const name of Object.keys(bottoms)) {
|
|
184
|
-
if (!isModEventName(name)
|
|
181
|
+
if (!isModEventName(name))
|
|
185
182
|
continue;
|
|
183
|
+
const seat = bottoms[name];
|
|
186
184
|
const st = stationOn(name, side);
|
|
187
|
-
if (st?.method
|
|
185
|
+
if (seat !== undefined && st?.method !== undefined)
|
|
186
|
+
reaches.set(name, { kind: "method", st, seat, pauses: st.method.pausesCallerBudget });
|
|
187
|
+
}
|
|
188
|
+
for (const name of Object.keys(entries)) {
|
|
189
|
+
if (!isModEventName(name))
|
|
188
190
|
continue;
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
+
const entry = entries[name];
|
|
192
|
+
const st = stationOn(name, side);
|
|
193
|
+
if (entry !== undefined && st?.call !== undefined)
|
|
194
|
+
reaches.set(name, { kind: "call", entry, pauses: st.call.pausesCallerBudget });
|
|
191
195
|
}
|
|
192
196
|
const bus = {
|
|
193
197
|
records,
|
|
@@ -215,26 +219,26 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
215
219
|
const inFrames = (fs, h) => fs.some((f) => f.plugin === h.reg.name && (f.kind === "dollar" || f.handler === h.index));
|
|
216
220
|
const caller = frames.slice(-1);
|
|
217
221
|
const above = frames.slice(0, -1);
|
|
218
|
-
const
|
|
222
|
+
const all = [];
|
|
223
|
+
const guards = [];
|
|
219
224
|
for (const h of mounted) {
|
|
220
225
|
if (!h.events.has(event) || !admits(participant, h) || !matcherMatches(h.matcher, e))
|
|
221
226
|
continue;
|
|
222
227
|
if (inFrames(caller, h))
|
|
223
228
|
continue;
|
|
224
229
|
if (!inFrames(above, h))
|
|
225
|
-
|
|
226
|
-
else if (h.
|
|
227
|
-
|
|
230
|
+
all.push({ h, reentry: false });
|
|
231
|
+
else if (h.observe)
|
|
232
|
+
continue;
|
|
233
|
+
else if (h.onCatch !== undefined)
|
|
234
|
+
all.push({ h, reentry: true });
|
|
235
|
+
else
|
|
236
|
+
guards.push(h);
|
|
228
237
|
}
|
|
229
|
-
return
|
|
238
|
+
return { all, guards };
|
|
230
239
|
}
|
|
231
|
-
async function callMethod(event, args, caller) {
|
|
232
|
-
const seat =
|
|
233
|
-
if (seat === undefined)
|
|
234
|
-
throw new Error(`${caller.origin.plugin}: $.${event}: no implementation on the ${side} bus`);
|
|
235
|
-
const st = stationOn(event, side);
|
|
236
|
-
if (st === undefined)
|
|
237
|
-
throw new Error(`${caller.origin.plugin}: $.${event}: not raised on the ${side} bus`);
|
|
240
|
+
async function callMethod(event, reach, args, caller) {
|
|
241
|
+
const { seat, st } = reach;
|
|
238
242
|
let e0 = args;
|
|
239
243
|
if (seat.enter !== undefined) {
|
|
240
244
|
const shaped = receivePayload(seat.enter(args), isProxy, engineObjectsOnce(args)());
|
|
@@ -249,10 +253,6 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
249
253
|
signal: caller.signal,
|
|
250
254
|
checkAnswer: methodAnswerViolation,
|
|
251
255
|
});
|
|
252
|
-
if (outcome.kind !== "aborted")
|
|
253
|
-
for (const s of outcome.skipped)
|
|
254
|
-
if (s.cause === "violation")
|
|
255
|
-
report(new Error(`${s.registration}: $.${event}: ${s.reason} — skipped`));
|
|
256
256
|
if (outcome.kind === "aborted")
|
|
257
257
|
throw new Error(`${caller.origin.plugin}: $.${event}: aborted`);
|
|
258
258
|
if (outcome.kind === "failed")
|
|
@@ -265,24 +265,28 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
265
265
|
}
|
|
266
266
|
function dollarFor(reg, frames, budget, signal, engines) {
|
|
267
267
|
const origin = Object.freeze({ plugin: reg.name, tier: reg.tier });
|
|
268
|
-
const
|
|
268
|
+
const caller = Object.freeze({ frames: Object.freeze([...frames, Object.freeze({ kind: "dollar", plugin: reg.name })]), origin, signal });
|
|
269
269
|
const namespaces = new Map();
|
|
270
|
-
for (const
|
|
271
|
-
const
|
|
272
|
-
namespaces.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
270
|
+
for (const event of new Set([...reaches.keys(), ...reg.callsBound])) {
|
|
271
|
+
const dot = event.indexOf(".");
|
|
272
|
+
const ns = namespaces.get(event.slice(0, dot)) ?? {};
|
|
273
|
+
namespaces.set(event.slice(0, dot), ns);
|
|
274
|
+
ns[event.slice(dot + 1)] = async (args) => {
|
|
275
|
+
if (!reg.callsBound.has(event))
|
|
276
|
+
throw new Error(`${reg.name}: $.${event}: not declared in uses.calls`);
|
|
277
|
+
const reach = reaches.get(event);
|
|
278
|
+
if (reach === undefined)
|
|
279
|
+
throw new Error(`${reg.name}: $.${event}: not callable on the ${side} bus in this version`);
|
|
276
280
|
const received = receivePayload(args === undefined ? {} : args, isProxy, engines());
|
|
277
281
|
if ("refusal" in received)
|
|
278
|
-
throw new TypeError(`${reg.name}: $.${
|
|
279
|
-
if (
|
|
282
|
+
throw new TypeError(`${reg.name}: $.${event}: takes one object — ${received.refusal}`);
|
|
283
|
+
if (reach.pauses)
|
|
280
284
|
budget.pause();
|
|
281
285
|
try {
|
|
282
|
-
return await callMethod(
|
|
286
|
+
return reach.kind === "method" ? await callMethod(event, reach, received.payload, caller) : await reach.entry(received.payload, caller);
|
|
283
287
|
}
|
|
284
288
|
finally {
|
|
285
|
-
if (
|
|
289
|
+
if (reach.pauses)
|
|
286
290
|
budget.resume();
|
|
287
291
|
}
|
|
288
292
|
};
|
|
@@ -290,12 +294,25 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
290
294
|
const out = {};
|
|
291
295
|
for (const [ns, fns] of namespaces)
|
|
292
296
|
out[ns] = Object.freeze(fns);
|
|
293
|
-
return Object.freeze({ ...out, plugin: Object.freeze({ name: reg.name }) });
|
|
297
|
+
return Object.freeze({ ...out, plugin: Object.freeze({ ...(namespaces.get("plugin") ?? {}), name: reg.name }) });
|
|
294
298
|
}
|
|
295
299
|
function runDispatch(event, st, e0, options) {
|
|
296
300
|
const frames = options.frames ?? [];
|
|
297
301
|
const origin = options.origin ?? ENGINE_ORIGIN;
|
|
298
|
-
const
|
|
302
|
+
const refusing = failureRefuses(st.failure);
|
|
303
|
+
const engineFault = (reason) => {
|
|
304
|
+
report(new Error(`engine: ${event}: ${reason} — the dispatch was not run`));
|
|
305
|
+
return Promise.resolve(Object.freeze({ kind: "failed", registration: ENGINE_ORIGIN.plugin, cause: "violation", reason: `${event}: ${reason}`, skipped: Object.freeze([]) }));
|
|
306
|
+
};
|
|
307
|
+
if (refusing && options.ranks === undefined)
|
|
308
|
+
return engineFault("a dispatch of an event whose failure column refuses names no answer ranks (the seat-trust law reads them)");
|
|
309
|
+
const { all, guards } = participants(event, e0, frames, options.participant);
|
|
310
|
+
const guard = refusing ? guards[0] : undefined;
|
|
311
|
+
if (guard !== undefined) {
|
|
312
|
+
const reason = "not run: the dispatch re-entered a frame it is part of, and it has no .catch to answer for it — a guard that cannot answer refuses";
|
|
313
|
+
report(new Error(`${guard.reg.name}: ${event}: ${reason} — failed closed: the dispatch ends here`));
|
|
314
|
+
return Promise.resolve(Object.freeze({ kind: "failed", registration: guard.reg.name, cause: "reentry", reason, skipped: Object.freeze([]) }));
|
|
315
|
+
}
|
|
299
316
|
const observers = all.filter((p) => !p.reentry && p.h.observe).map((p) => p.h);
|
|
300
317
|
const chain = all.filter((p) => !p.h.observe).map((p) => p.h);
|
|
301
318
|
const reentered = new Set(all.filter((p) => p.reentry).map((p) => p.h));
|
|
@@ -305,6 +322,8 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
305
322
|
const dispatchAc = new AbortController();
|
|
306
323
|
const terminal = { kind: "terminal" };
|
|
307
324
|
let settled = false;
|
|
325
|
+
let bottomStarted = false;
|
|
326
|
+
const ownMs = (ms) => (st.budgetMs === undefined ? ms : Math.min(ms, st.budgetMs));
|
|
308
327
|
let graceTimer;
|
|
309
328
|
let resolveOut = () => { };
|
|
310
329
|
let rejectOut = () => { };
|
|
@@ -313,15 +332,16 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
313
332
|
rejectOut = rej;
|
|
314
333
|
});
|
|
315
334
|
const signal = options.signal;
|
|
335
|
+
const aborted = () => (bottomStarted ? { kind: "aborted", bottomStarted: true } : { kind: "aborted" });
|
|
316
336
|
const onAbort = () => {
|
|
317
337
|
if (settled)
|
|
318
338
|
return;
|
|
319
339
|
if (st.abort === "grace") {
|
|
320
|
-
graceTimer ??= setTimeout(() => finish(
|
|
340
|
+
graceTimer ??= setTimeout(() => finish(aborted()), POST_ABORT_GRACE_MS);
|
|
321
341
|
unrefTimer(graceTimer);
|
|
322
342
|
}
|
|
323
343
|
else
|
|
324
|
-
finish(
|
|
344
|
+
finish(aborted());
|
|
325
345
|
};
|
|
326
346
|
function finish(o) {
|
|
327
347
|
if (settled)
|
|
@@ -348,23 +368,66 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
348
368
|
run.then(drop, drop);
|
|
349
369
|
};
|
|
350
370
|
const engines = engineObjectsOnce(e0);
|
|
371
|
+
const eventSealed = (() => {
|
|
372
|
+
let found;
|
|
373
|
+
return () => (found ??= sealedObjects([e0]));
|
|
374
|
+
})();
|
|
351
375
|
const handOn = (passed) => {
|
|
352
376
|
if (passed === e0)
|
|
353
377
|
return { snap: e0 };
|
|
354
378
|
const handed = handedOn(st.pinned, e0, passed, engines(), isProxy);
|
|
355
379
|
return "violation" in handed ? handed : { snap: handed.payload };
|
|
356
380
|
};
|
|
357
|
-
const
|
|
381
|
+
const trustFloors = new Map();
|
|
382
|
+
const NO_FLOOR = Object.freeze(MOD_SEATS.map(() => 0));
|
|
383
|
+
const seatRank = (seat) => MOD_SEATS.indexOf(seat);
|
|
384
|
+
const seatTrust = (taken, s, h, i, tiers) => {
|
|
385
|
+
const ranks = options.ranks;
|
|
386
|
+
if (ranks === undefined)
|
|
387
|
+
return undefined;
|
|
388
|
+
const mine = seatRank(h.reg.seat);
|
|
389
|
+
let rank;
|
|
390
|
+
try {
|
|
391
|
+
rank = ranks.of(taken);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return "returned an answer the station cannot rank";
|
|
395
|
+
}
|
|
396
|
+
const answered = s.pending === 0 && s.answers.length > 0;
|
|
397
|
+
let floor;
|
|
398
|
+
if (answered) {
|
|
399
|
+
const inherited = trustFloors.get(s.answers[s.answers.length - 1]) ?? NO_FLOOR;
|
|
400
|
+
if (rank < (inherited[mine] ?? 0))
|
|
401
|
+
return `answered less strictly than a more trusted seat's handler below it did — a ${h.reg.seat} answer never stands over a stricter one of a more trusted seat`;
|
|
402
|
+
floor = inherited;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const unrun = chain.slice(i + 1).filter((m) => !bypassed(tiers, m.reg.tier)).map((m) => seatRank(m.reg.seat));
|
|
406
|
+
const trusted = unrun.length > 0 ? Math.min(...unrun) : MOD_SEATS.length;
|
|
407
|
+
if (trusted < mine && rank < ranks.top) {
|
|
408
|
+
return `answered ${s.called ? "while its next(e) still ran" : "without calling next(e)"}, with a ${MOD_SEATS[trusted]} handler below it that did not judge — a ${h.reg.seat} answer stands in place of theirs only when it refuses outright`;
|
|
409
|
+
}
|
|
410
|
+
floor = NO_FLOOR.map((_, k) => (k > trusted ? ranks.top : 0));
|
|
411
|
+
}
|
|
412
|
+
trustFloors.set(taken, floor.map((f, k) => (mine < k ? rank : f)));
|
|
413
|
+
return undefined;
|
|
414
|
+
};
|
|
415
|
+
const takeAnswer = (answer, s, e, h, i, tiers) => {
|
|
416
|
+
const handedUp = s.answers.some((a) => Object.is(a, answer));
|
|
358
417
|
let taken = answer;
|
|
359
|
-
if (!
|
|
360
|
-
const
|
|
418
|
+
if (!handedUp) {
|
|
419
|
+
const held = { has: (v) => eventSealed().has(v) || s.sealed?.has(v) === true };
|
|
420
|
+
const received = receiveAnswer(answer, held, isProxy);
|
|
361
421
|
if ("refusal" in received)
|
|
362
422
|
return { violation: received.refusal };
|
|
363
423
|
taken = received.answer;
|
|
364
424
|
}
|
|
365
425
|
const below = s.pending > 0 ? [...s.answers, NEXT_IN_FLIGHT] : s.answers;
|
|
366
|
-
const why = answerViolation(taken, options.checkAnswer, e, answererOf(h), below);
|
|
367
|
-
|
|
426
|
+
const why = answerViolation(taken, options.checkAnswer, e, answererOf(h), below, Object.freeze({ bottomStarted }));
|
|
427
|
+
if (why !== undefined)
|
|
428
|
+
return { violation: why };
|
|
429
|
+
const trust = handedUp ? undefined : seatTrust(taken, s, h, i, tiers);
|
|
430
|
+
return trust !== undefined ? { violation: trust } : { answer: taken };
|
|
368
431
|
};
|
|
369
432
|
const traceOf = (sink, index, plugin, tier, outcome, ms) => {
|
|
370
433
|
sink.push(Object.freeze({ index, plugin, tier, outcome, ms }));
|
|
@@ -383,6 +446,7 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
383
446
|
run.then((v) => {
|
|
384
447
|
state.lastValue = { v };
|
|
385
448
|
state.answers.push(v);
|
|
449
|
+
state.sealed = new Set([...(state.sealed ?? []), ...sealedObjects([v])]);
|
|
386
450
|
stopCounting();
|
|
387
451
|
}, (err) => {
|
|
388
452
|
state.belowError = err;
|
|
@@ -397,8 +461,9 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
397
461
|
const h = chain[i];
|
|
398
462
|
if (h === undefined) {
|
|
399
463
|
const t0 = Date.now();
|
|
464
|
+
bottomStarted = true;
|
|
400
465
|
try {
|
|
401
|
-
const v = answerUpward(await options.bottom(e, Object.freeze({ frames: fr, signal: dispatchAc.signal, origin })));
|
|
466
|
+
const v = answerUpward(await options.bottom(e, Object.freeze({ frames: fr, signal: dispatchAc.signal, origin })), isProxy);
|
|
402
467
|
traceOf(sink, i, "core", "core", "returned", Date.now() - t0);
|
|
403
468
|
return v;
|
|
404
469
|
}
|
|
@@ -432,7 +497,8 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
432
497
|
const name = h.reg.name;
|
|
433
498
|
const ac = new AbortController();
|
|
434
499
|
live.add(ac);
|
|
435
|
-
const
|
|
500
|
+
const budgetMs = ownMs(h.reg.budgetMs);
|
|
501
|
+
const budget = createSelfBudget(budgetMs, () => ac.abort(new Error(`${name} ran past its ${budgetMs}ms budget`)));
|
|
436
502
|
const myFrames = Object.freeze([...fr, Object.freeze({ kind: "handler", plugin: name, handler: h.index })]);
|
|
437
503
|
const state = {
|
|
438
504
|
pending: 0,
|
|
@@ -475,9 +541,9 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
475
541
|
try {
|
|
476
542
|
const raced = await Promise.race([handlerP, budget.expired]);
|
|
477
543
|
if (raced === "expired" && budget.isExpired())
|
|
478
|
-
failure = { cause: "timeout", reason: `ran past its ${
|
|
544
|
+
failure = { cause: "timeout", reason: `ran past its ${budgetMs}ms budget` };
|
|
479
545
|
else {
|
|
480
|
-
const taken = takeAnswer(raced, state, e, h);
|
|
546
|
+
const taken = takeAnswer(raced, state, e, h, i, floors);
|
|
481
547
|
if ("violation" in taken)
|
|
482
548
|
failure = { cause: "violation", reason: state.violated ?? taken.violation };
|
|
483
549
|
else if (state.violated !== undefined)
|
|
@@ -521,11 +587,14 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
521
587
|
return caught.answer;
|
|
522
588
|
}
|
|
523
589
|
if (failure.cause === "threw" && state.belowError !== undefined && failure.error === state.belowError) {
|
|
590
|
+
if (caught !== undefined)
|
|
591
|
+
report(new Error(`${name}: ${event}: ${caught.problem} — what the run below threw stands`));
|
|
524
592
|
traceOf(sink, i, name, h.reg.tier, "rejected", ms);
|
|
525
593
|
throw failure.error;
|
|
526
594
|
}
|
|
527
595
|
const reason = caught !== undefined ? `${failure.reason}; ${caught.problem}` : failure.reason;
|
|
528
|
-
|
|
596
|
+
const effectStands = st.failure === "refuse_until_run" && bottomStarted && state.last !== undefined;
|
|
597
|
+
if (refusing && !effectStands) {
|
|
529
598
|
if (failure.cause !== "violation")
|
|
530
599
|
report(new Error(`${name}: ${event}: ${reason} — failed closed: the dispatch ends here`));
|
|
531
600
|
traceOf(sink, i, name, h.reg.tier, "rejected", ms);
|
|
@@ -534,8 +603,7 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
534
603
|
}
|
|
535
604
|
skipped.push(Object.freeze({ registration: name, cause: failure.cause, reason }));
|
|
536
605
|
const effect = state.last !== undefined ? MA : HA;
|
|
537
|
-
|
|
538
|
-
report(new Error(`${name}: ${event}: ${reason} — ${effect}`));
|
|
606
|
+
report(new Error(`${name}: ${event}: ${reason} — ${effect}`));
|
|
539
607
|
if (state.last !== undefined) {
|
|
540
608
|
traceOf(sink, i, name, h.reg.tier, failure.cause === "timeout" ? "expired" : "kept", ms);
|
|
541
609
|
return state.last;
|
|
@@ -549,7 +617,8 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
549
617
|
return { problem: "no .catch" };
|
|
550
618
|
const ac = new AbortController();
|
|
551
619
|
live.add(ac);
|
|
552
|
-
const
|
|
620
|
+
const catchMs = ownMs(CATCH_BUDGET_MS);
|
|
621
|
+
const budget = createSelfBudget(catchMs, () => ac.abort(new Error(`${h.reg.name}: its .catch ran past its ${catchMs}ms budget`)));
|
|
553
622
|
let done = false;
|
|
554
623
|
let violated;
|
|
555
624
|
const replay = (passed, nextFloors) => {
|
|
@@ -570,7 +639,7 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
570
639
|
return startBelow(state, budget, i, handed.snap, nextFloors, myFrames);
|
|
571
640
|
};
|
|
572
641
|
const message = failure.cause === "threw" ? describe(failure.error) : failure.cause === "violation" ? failure.reason : undefined;
|
|
573
|
-
const error = Object.freeze({ kind: failure.cause, ...(message !== undefined ? { message } : {}), budget:
|
|
642
|
+
const error = Object.freeze({ kind: failure.cause, ...(message !== undefined ? { message } : {}), budget: catchMs });
|
|
574
643
|
const next = makeNext({ budget, signal: ac.signal, trace: () => Object.freeze([...state.below]) }, (x) => replay(x, floors), (x, tier) => {
|
|
575
644
|
const why = nextToRefusal(h.reg.tier, tier);
|
|
576
645
|
return why !== undefined ? Promise.reject(new Error(`${h.reg.name}: ${why}`)) : replay(x, [...floors, { from: h.reg.tier, to: tier }]);
|
|
@@ -578,10 +647,10 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
578
647
|
try {
|
|
579
648
|
const raced = await Promise.race([invokeNow(() => onCatch(dollarFor(h.reg, myFrames, budget, ac.signal, engines), e, next)), budget.expired]);
|
|
580
649
|
if (raced === "expired" && budget.isExpired())
|
|
581
|
-
return { problem: `its .catch ran past its ${
|
|
650
|
+
return { problem: `its .catch ran past its ${catchMs}ms budget` };
|
|
582
651
|
if (violated !== undefined)
|
|
583
652
|
return { problem: `its .catch ${violated}` };
|
|
584
|
-
const taken = takeAnswer(raced, state, e, h);
|
|
653
|
+
const taken = takeAnswer(raced, state, e, h, i, floors);
|
|
585
654
|
return "violation" in taken ? { problem: `its .catch ${taken.violation}` } : { answer: taken.answer };
|
|
586
655
|
}
|
|
587
656
|
catch (err) {
|
|
@@ -598,7 +667,8 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
598
667
|
const name = h.reg.name;
|
|
599
668
|
const ac = new AbortController();
|
|
600
669
|
live.add(ac);
|
|
601
|
-
const
|
|
670
|
+
const budgetMs = ownMs(h.reg.budgetMs);
|
|
671
|
+
const budget = createSelfBudget(budgetMs, () => ac.abort(new Error(`${name} ran past its ${budgetMs}ms budget`)));
|
|
602
672
|
const myFrames = Object.freeze([...fr, Object.freeze({ kind: "handler", plugin: name, handler: h.index })]);
|
|
603
673
|
const refuse = () => Promise.reject(new Error(`${name}: an observe handler is not in the chain; next() is unavailable`));
|
|
604
674
|
const next = makeNext({ budget, signal: ac.signal, trace: () => Object.freeze([]) }, refuse, refuse);
|
|
@@ -610,7 +680,7 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
610
680
|
try {
|
|
611
681
|
const raced = await Promise.race([invokeNow(() => h.fn(dollarFor(h.reg, myFrames, budget, ac.signal, engines), detachedCopy(e0), next)), budget.expired]);
|
|
612
682
|
if (raced === "expired" && budget.isExpired())
|
|
613
|
-
throw new Error(`ran past its ${
|
|
683
|
+
throw new Error(`ran past its ${budgetMs}ms budget`);
|
|
614
684
|
verdict(raced);
|
|
615
685
|
}
|
|
616
686
|
catch (err) {
|
|
@@ -623,9 +693,10 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
623
693
|
budget.clear();
|
|
624
694
|
const cac = new AbortController();
|
|
625
695
|
live.add(cac);
|
|
626
|
-
const
|
|
696
|
+
const catchMs = ownMs(CATCH_BUDGET_MS);
|
|
697
|
+
const catchBudget = createSelfBudget(catchMs, () => cac.abort(new Error(`${name}: its .catch ran past its ${catchMs}ms budget`)));
|
|
627
698
|
const cnext = makeNext({ budget: catchBudget, signal: cac.signal, trace: () => Object.freeze([]) }, refuse, refuse, {
|
|
628
|
-
error: Object.freeze({ kind: cause, ...(cause === "threw" ? { message: describe(err) } : {}), budget:
|
|
699
|
+
error: Object.freeze({ kind: cause, ...(cause === "threw" ? { message: describe(err) } : {}), budget: catchMs }),
|
|
629
700
|
called: false,
|
|
630
701
|
});
|
|
631
702
|
try {
|
|
@@ -662,7 +733,7 @@ function createBus(side, mounted, bottoms, report, records, describe, isProxy) {
|
|
|
662
733
|
if (settled)
|
|
663
734
|
return;
|
|
664
735
|
const answer = await invoke(0, e0, [], frames, root);
|
|
665
|
-
while (
|
|
736
|
+
while (refusing && pendingRuns.size > 0 && !settled)
|
|
666
737
|
await Promise.allSettled([...pendingRuns]);
|
|
667
738
|
finish({ kind: "answered", answer, ...(producers.has(answer) ? { answeredBy: producers.get(answer) } : {}), skipped: Object.freeze([...skipped]) });
|
|
668
739
|
})().catch((err) => {
|
package/dist/events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const KERNEL_CONTRACT_VERSION =
|
|
1
|
+
export declare const KERNEL_CONTRACT_VERSION = 3;
|
|
2
2
|
export declare const MOD_TIERS: readonly ["prepend", "user", "append", "builtin", "core"];
|
|
3
3
|
export type ModTier = (typeof MOD_TIERS)[number];
|
|
4
4
|
export type ModRegistrationTier = Exclude<ModTier, "builtin" | "core">;
|
|
@@ -7,8 +7,9 @@ export type ModSeat = (typeof MOD_SEATS)[number];
|
|
|
7
7
|
export declare const MOD_EVENT_SIDES: readonly ["engine", "surface", "both"];
|
|
8
8
|
export type ModEventSide = (typeof MOD_EVENT_SIDES)[number];
|
|
9
9
|
export type ModBusSide = Exclude<ModEventSide, "both">;
|
|
10
|
-
export declare const MOD_FAILURE_ANSWERS: readonly ["refuse", "skip"];
|
|
10
|
+
export declare const MOD_FAILURE_ANSWERS: readonly ["refuse", "skip", "refuse_until_run"];
|
|
11
11
|
export type ModFailureAnswer = (typeof MOD_FAILURE_ANSWERS)[number];
|
|
12
|
+
export declare function failureRefuses(failure: ModFailureAnswer): boolean;
|
|
12
13
|
export declare const MOD_ABORT_POSTURES: readonly ["immediate", "grace", "caller"];
|
|
13
14
|
export type ModAbortPosture = (typeof MOD_ABORT_POSTURES)[number];
|
|
14
15
|
export declare const NEXT_TO_TABLE: Readonly<{
|
|
@@ -36,6 +37,10 @@ export interface ModStation {
|
|
|
36
37
|
readonly method?: {
|
|
37
38
|
readonly pausesCallerBudget: boolean;
|
|
38
39
|
};
|
|
40
|
+
readonly call?: {
|
|
41
|
+
readonly pausesCallerBudget: boolean;
|
|
42
|
+
};
|
|
43
|
+
readonly budgetMs?: number;
|
|
39
44
|
}
|
|
40
45
|
export interface ModEventRow {
|
|
41
46
|
readonly side: ModEventSide;
|
|
@@ -197,4 +202,9 @@ export declare function expandModPattern(pattern: unknown): {
|
|
|
197
202
|
} | {
|
|
198
203
|
readonly refusal: string;
|
|
199
204
|
};
|
|
205
|
+
export declare function expandOnSide(pattern: unknown, side: ModBusSide): {
|
|
206
|
+
readonly names: readonly ModEventName[];
|
|
207
|
+
} | {
|
|
208
|
+
readonly refusal: string;
|
|
209
|
+
};
|
|
200
210
|
export declare function describe(value: unknown): string;
|