@sema-agent/mods-kernel 7.55.0 → 7.56.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 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 — which keys, and how a pinned key is judged) change.
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
- | Admission and the bus | `admitMods` `ModFrame` `ModBottomContext` `ModMethodBottom` `ModMethodSeat` `ModMethodBottoms` `ModDispatchOutcome` `ModDispatchOptions` `ModBus` `ModMountedHandler` `ModBusInput` |
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
 
@@ -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: ModEventPayload, options: ModDispatchOptions): Promise<ModDispatchOutcome>;
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 { expandModPattern, isModEventName, MOD_SEATS, nextToRefusal, stationOn } from "./events.js";
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
- checkAnswer: (a, judged, by) => a === admitted
29
- ? undefined
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 = expandModPattern(pattern);
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 methods = [];
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) || bottoms[name] === undefined)
181
+ if (!isModEventName(name))
185
182
  continue;
183
+ const seat = bottoms[name];
186
184
  const st = stationOn(name, side);
187
- if (st?.method === undefined)
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 dot = name.indexOf(".");
190
- methods.push({ event: name, ns: name.slice(0, dot), method: name.slice(dot + 1), pauses: st.method.pausesCallerBudget });
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 out = [];
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
- out.push({ h, reentry: false });
226
- else if (h.onCatch !== undefined && !h.observe)
227
- out.push({ h, reentry: true });
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 out;
238
+ return { all, guards };
230
239
  }
231
- async function callMethod(event, args, caller) {
232
- const seat = bottoms[event];
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 callerFrames = Object.freeze([...frames, Object.freeze({ kind: "dollar", plugin: reg.name })]);
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 m of methods) {
271
- const ns = namespaces.get(m.ns) ?? {};
272
- namespaces.set(m.ns, ns);
273
- ns[m.method] = async (args) => {
274
- if (!reg.callsBound.has(m.event))
275
- throw new Error(`${reg.name}: $.${m.event}: not declared in uses.calls`);
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}: $.${m.event}: takes one object — ${received.refusal}`);
279
- if (m.pauses)
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(m.event, received.payload, { frames: callerFrames, origin, signal });
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 (m.pauses)
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 all = participants(event, e0, frames, options.participant);
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({ kind: "aborted" }), POST_ABORT_GRACE_MS);
340
+ graceTimer ??= setTimeout(() => finish(aborted()), POST_ABORT_GRACE_MS);
321
341
  unrefTimer(graceTimer);
322
342
  }
323
343
  else
324
- finish({ kind: "aborted" });
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 takeAnswer = (answer, s, e, h) => {
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 (!s.answers.some((a) => Object.is(a, answer))) {
360
- const received = receiveAnswer(answer, engines(), isProxy);
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
- return why !== undefined ? { violation: why } : { answer: taken };
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 budget = createSelfBudget(h.reg.budgetMs, () => ac.abort(new Error(`${name} ran past its ${h.reg.budgetMs}ms budget`)));
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 ${h.reg.budgetMs}ms budget` };
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
- if (st.failure === "refuse") {
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
- if (failure.cause !== "violation")
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 budget = createSelfBudget(CATCH_BUDGET_MS, () => ac.abort(new Error(`${h.reg.name}: its .catch ran past its ${CATCH_BUDGET_MS}ms budget`)));
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: CATCH_BUDGET_MS });
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 ${CATCH_BUDGET_MS}ms budget` };
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 budget = createSelfBudget(h.reg.budgetMs, () => ac.abort(new Error(`${name} ran past its ${h.reg.budgetMs}ms budget`)));
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 ${h.reg.budgetMs}ms budget`);
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 catchBudget = createSelfBudget(CATCH_BUDGET_MS, () => cac.abort(new Error(`${name}: its .catch ran past its ${CATCH_BUDGET_MS}ms budget`)));
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: CATCH_BUDGET_MS }),
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 (st.failure === "refuse" && pendingRuns.size > 0 && !settled)
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 = 2;
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;