@cubicecho/agent-core 2.2.4 → 2.4.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/dist/events.js CHANGED
@@ -9,35 +9,40 @@
9
9
  * In memory on purpose: it is debugging output, worth nothing once the run has finished and its
10
10
  * outcome is in the database. Nothing here survives a restart, and nothing here is the record.
11
11
  */
12
- /** How many events one run keeps for a watcher that joins late. A chatty run loses its oldest. */
13
- const MAX_EVENTS = 1000;
12
+ /** The numbers a run of the shape this bus was written for wants. */
13
+ const DEFAULTS = {
14
+ maxEvents: 1000,
15
+ trimSlack: 256,
16
+ retainMs: 60_000,
17
+ retainUnendedMs: 30 * 60_000,
18
+ };
19
+ /** What is in force now. Read where it is used, so a change applies from the next event. */
20
+ let limits = { ...DEFAULTS };
14
21
  /**
15
- * How far past the cap the backlog is allowed to run before it is trimmed.
22
+ * Changes what the bus keeps, for a process whose runs are not shaped like the ones these
23
+ * defaults were chosen for.
16
24
  *
17
- * Dropping the oldest event on every push means shifting a thousand-element array tens of
18
- * thousands of times over a reasoning run the one thing in here that would ever show up in a
19
- * profile. Trimming in batches makes it a few dozen splices instead, at the cost of the backlog
20
- * sometimes being a little longer than the cap, which nothing depends on.
21
- */
22
- const TRIM_SLACK = 256;
23
- /** How long a finished run stays readable, for a watcher that arrives just after the end. */
24
- const RETAIN_MS = 60_000;
25
- /**
26
- * The same for a run that has not said `done`, which is a far more dangerous thing to drop.
25
+ * The bus is one module-level thing rather than an object a caller holds, so this is too: it is
26
+ * a deployment's setting, said once at startup, and not something to move around under a run.
27
+ * What it costs is memory against how much of a run a late or slow watcher can still read
28
+ * a server with hundreds of concurrent runs wants a smaller backlog, and one whose tool calls
29
+ * take an hour wants a longer `retainUnendedMs` than the thirty minutes assumed here.
27
30
  *
28
- * A finished run has nothing more to say, so forgetting it a minute later costs a late watcher
29
- * a backlog and nothing else. An unfinished one is still writing: `touched` only moves on
30
- * `emit`, so a live run that spends a minute inside one slow tool call looked exactly like an
31
- * abandoned one and was reaped out from under itself. What made that more than a lost backlog
32
- * is that the next `emit` builds a fresh stream with `seq` back at zero — and `seq` is the
33
- * field `RunEvent` documents for ordering and de-duplication, so a client that reconnects
34
- * across the gap discards the new events as ones it has already seen.
31
+ * Changes apply from the next event and the next sweep. Nothing already buffered is trimmed to
32
+ * a cap that has just come down, because the trim happens on push; the backlog settles to the
33
+ * new number as the run goes on.
35
34
  *
36
- * The sweep still has to reap them, because a run killed by a signal or simply forgotten
37
- * reaches no `done` either. This is the backstop for a caller that never says so; `endRun` is
38
- * for one that knows.
35
+ * @param options The bounds to change. A field left out or given anything that is not a
36
+ * number above zero keeps what it has, so a partial or a half-built config narrows nothing.
37
+ * @returns Everything in force afterwards, including what this call did not change.
39
38
  */
40
- const RETAIN_UNENDED_MS = 30 * 60_000;
39
+ export function configureEvents(options = {}) {
40
+ for (const [name, value] of Object.entries(options)) {
41
+ if (typeof value === "number" && value > 0)
42
+ limits[name] = value;
43
+ }
44
+ return { ...limits };
45
+ }
41
46
  const streams = new Map();
42
47
  const streamFor = (runId) => {
43
48
  const existing = streams.get(runId);
@@ -72,7 +77,7 @@ function sweep() {
72
77
  for (const [runId, stream] of streams) {
73
78
  if (stream.listeners.size)
74
79
  continue;
75
- if (stream.touched <= now - (stream.ended ? RETAIN_MS : RETAIN_UNENDED_MS))
80
+ if (stream.touched <= now - (stream.ended ? limits.retainMs : limits.retainUnendedMs))
76
81
  streams.delete(runId);
77
82
  }
78
83
  scheduleSweep();
@@ -80,7 +85,7 @@ function sweep() {
80
85
  function scheduleSweep() {
81
86
  if (sweeping || streams.size === 0)
82
87
  return;
83
- sweeping = setTimeout(sweep, RETAIN_MS);
88
+ sweeping = setTimeout(sweep, limits.retainMs);
84
89
  sweeping.unref?.();
85
90
  }
86
91
  /**
@@ -128,8 +133,8 @@ export function emit(runId, input) {
128
133
  };
129
134
  stream.events.push(event);
130
135
  stream.touched = at;
131
- if (stream.events.length > MAX_EVENTS + TRIM_SLACK) {
132
- stream.events.splice(0, stream.events.length - MAX_EVENTS);
136
+ if (stream.events.length > limits.maxEvents + limits.trimSlack) {
137
+ stream.events.splice(0, stream.events.length - limits.maxEvents);
133
138
  }
134
139
  // Kept for a moment so a watcher that arrives just after the end still sees how it went,
135
140
  // then dropped: a finished run's record is the row, not this.
@@ -153,20 +158,26 @@ export function emit(runId, input) {
153
158
  * inside the retention window — reads the same story as one that was there from the start.
154
159
  *
155
160
  * @param runId The run to follow. One that has not started yet is waited on, not refused.
161
+ * @param signal Stops following. The only other way out is the run's own `done`, and a watcher
162
+ * with no way out is a leak rather than a lost backlog: the sweep below skips any stream a
163
+ * listener is on, so a run that dies without `done` pins its backlog for the life of the process.
164
+ * Returning the generator is not that way out — parked on the promise at the foot of this
165
+ * function it is suspended at an `await` rather than at a `yield`, and a `return()` there is
166
+ * queued behind a promise only the next event can settle. An abort resolves that promise itself.
156
167
  */
157
- export async function* watch(runId) {
168
+ export async function* watch(runId, signal) {
158
169
  const stream = streamFor(runId);
159
170
  // A cursor rather than `shift()`. Draining a backlog an event at a time off the front of an
160
171
  // array is a copy of the whole array per event, which on the ten-thousand-delta run this bus
161
172
  // is built for is the one quadratic left in the file. The prefix behind the cursor is dropped
162
- // in one `slice` per `MAX_EVENTS` instead — the same amortised trade the bus itself makes.
173
+ // in one `slice` per `maxEvents` instead — the same amortised trade the bus itself makes.
163
174
  let queue = [...stream.events];
164
175
  let head = 0;
165
176
  let dropped = 0;
166
177
  let wake = null;
167
178
  const listener = (event) => {
168
179
  queue.push(event);
169
- // The bus caps its own backlog at `MAX_EVENTS`; without this the watcher downstream of it
180
+ // The bus caps its own backlog at `maxEvents`; without this the watcher downstream of it
170
181
  // had no cap at all, so a client too slow to keep up held every delta a run ever emitted.
171
182
  // The oldest go, which is what the backlog does, and the gap is reported once below.
172
183
  //
@@ -174,8 +185,8 @@ export async function* watch(runId) {
174
185
  // the slots behind it, to be freed by the compaction in the drain below — which a consumer
175
186
  // that has stalled does not reach, and a stalled consumer is the whole reason for the cap.
176
187
  // It read as capped and held every event anyway: 16MB where the cap promises a third of one.
177
- const cut = queue.length - head - MAX_EVENTS;
178
- if (cut > TRIM_SLACK) {
188
+ const cut = queue.length - head - limits.maxEvents;
189
+ if (cut > limits.trimSlack) {
179
190
  queue = queue.slice(head + cut);
180
191
  head = 0;
181
192
  dropped += cut;
@@ -183,9 +194,16 @@ export async function* watch(runId) {
183
194
  wake?.();
184
195
  };
185
196
  stream.listeners.add(listener);
197
+ // The same wake the listener uses: an abort is another reason to stop waiting, and what the
198
+ // loop does about it is decided in one place below rather than here.
199
+ const onAbort = () => wake?.();
200
+ signal?.addEventListener("abort", onAbort, { once: true });
186
201
  try {
187
202
  for (;;) {
188
- while (head < queue.length) {
203
+ // Guarding the drain rather than sitting after it, so an already-aborted signal leaves
204
+ // without replaying the backlog and one raised mid-drain stops at the next event instead
205
+ // of finishing the queue first.
206
+ while (!signal?.aborted && head < queue.length) {
189
207
  const event = queue[head++];
190
208
  // What is behind the cursor is released rather than left there. Resetting only on catch-up
191
209
  // was not enough: a watcher that keeps pace but never quite empties the queue never
@@ -194,7 +212,7 @@ export async function* watch(runId) {
194
212
  queue = [];
195
213
  head = 0;
196
214
  }
197
- else if (head > MAX_EVENTS) {
215
+ else if (head > limits.maxEvents) {
198
216
  queue = queue.slice(head);
199
217
  head = 0;
200
218
  }
@@ -227,6 +245,8 @@ export async function* watch(runId) {
227
245
  if (event.kind === "done")
228
246
  return;
229
247
  }
248
+ if (signal?.aborted)
249
+ return;
230
250
  await new Promise((resolve) => {
231
251
  wake = resolve;
232
252
  });
@@ -234,6 +254,10 @@ export async function* watch(runId) {
234
254
  }
235
255
  }
236
256
  finally {
257
+ // `once` covers the abort that fired; this is for the one that never did, which would
258
+ // otherwise hold this generator and its queue alive for as long as the caller holds the
259
+ // signal — a run's whole backlog kept by a watcher that finished on `done`.
260
+ signal?.removeEventListener("abort", onAbort);
237
261
  stream.listeners.delete(listener);
238
262
  // A watcher can name a run that has not started, or will never start. Nothing was recorded
239
263
  // under it, so nothing is left behind either — and a run that has ended has nothing more to
@@ -251,6 +275,11 @@ export async function* watch(runId) {
251
275
  /**
252
276
  * The backlog alone, for a caller that wants a snapshot rather than a subscription.
253
277
  *
278
+ * The array is a copy; the events in it are not. They are the same objects the bus holds and
279
+ * every watcher was handed, so writing to one rewrites the run for everybody — which is what
280
+ * `fold` copies to avoid, and this is the other half of the same warning. Read them, or copy
281
+ * what you mean to change.
282
+ *
254
283
  * @param runId The run to read. An unknown or already-swept run gives an empty array.
255
284
  */
256
285
  export const history = (runId) => [...(streams.get(runId)?.events ?? [])];
@@ -260,12 +289,18 @@ export const history = (runId) => [...(streams.get(runId)?.events ?? [])];
260
289
  * Named for what it forgets rather than bare `reset`, which sat in a consumer's imports beside
261
290
  * `resetAll`, `resetClients`, `resetCapabilities` and `resetHints` saying nothing about which
262
291
  * of the five it was — `reset.ts` had to alias it on the way in to stay readable.
292
+ *
293
+ * `configureEvents` is undone too, for the reason `reset.ts` gives about latches: a bus left
294
+ * holding a cap one test set is the same order-dependent suite, passing where that test ran
295
+ * first and failing where it did not. A consumer that configures at startup and resets at
296
+ * teardown configures again, which is the same line it already wrote once.
263
297
  */
264
298
  export const resetEvents = () => {
265
299
  streams.clear();
266
300
  if (sweeping)
267
301
  clearTimeout(sweeping);
268
302
  sweeping = null;
303
+ limits = { ...DEFAULTS };
269
304
  };
270
305
  /**
271
306
  * Consecutive tokens of one kind are one thing being said, not hundreds of things.
@@ -0,0 +1,271 @@
1
+ import type OpenAI from "openai";
2
+ /**
3
+ * Lifecycle hooks, from the host's side: what a session looks like to them, where their context
4
+ * lands in a request, and what is said about each one.
5
+ *
6
+ * Running a hook is not here. What a hook *is* — an MCP tool call in `@cubicecho/agent-mcp-pool`,
7
+ * something else in another host — is the runner's business, and this takes the runner as a
8
+ * function. The types are the pool's shapes restated rather than imported, so the pool's
9
+ * `runHooks` is a runner as it stands and its outcomes pass straight through, without this
10
+ * package depending on it.
11
+ */
12
+ /**
13
+ * A point in a session a hook can be bound to. Named after Claude Code's hooks of the same shape
14
+ * — `sessionStart` (SessionStart), `beforeTurn` (UserPromptSubmit), `afterTurn` (Stop),
15
+ * `beforeCompact` (PreCompact), `sessionEnd` (SessionEnd) — plus `sessionDelete`, for when the host
16
+ * deletes a session's record.
17
+ */
18
+ export type HookEvent = "sessionStart" | "beforeTurn" | "afterTurn" | "beforeCompact" | "sessionEnd" | "sessionDelete";
19
+ /** Every event a hook can be bound to, in the order a session meets them. */
20
+ export declare const HOOK_EVENTS: readonly HookEvent[];
21
+ /**
22
+ * The events whose hooks run before a request, and so the only ones whose output can reach it.
23
+ * Anything later runs once the model has already answered.
24
+ */
25
+ export declare const INJECT_EVENTS: ReadonlySet<HookEvent>;
26
+ /**
27
+ * One message of a session as a hook is handed it. The shape a memory server's `remember` takes,
28
+ * so a template can pass a turn straight through.
29
+ */
30
+ export interface HookMessage {
31
+ speaker: string;
32
+ text: string;
33
+ /** Stable across retries, so a server that dedups on it files a re-sent turn once. */
34
+ uuid: string;
35
+ }
36
+ /**
37
+ * What a host knows at an event. Every field but `session` is optional because no event carries
38
+ * all of them.
39
+ */
40
+ export interface HookContext {
41
+ session: {
42
+ id: string;
43
+ };
44
+ /** Which program is running the session, for a hook shared by several. */
45
+ host?: string;
46
+ /** ISO 8601. */
47
+ now?: string;
48
+ /** The user's message this turn, or the session's opening one. */
49
+ prompt?: string;
50
+ /** The assistant's final text. */
51
+ reply?: string;
52
+ turn?: {
53
+ /** Which turn of the session this is, from 0. See `turnIndex`. */
54
+ index: number;
55
+ /** The turn's user and assistant text, tool traffic left out. See `turnMessages`. */
56
+ messages?: HookMessage[];
57
+ };
58
+ /** The messages about to be summarised away. */
59
+ compacting?: HookMessage[];
60
+ /** Message indexes of that range, `through` exclusive. */
61
+ range?: {
62
+ from: number;
63
+ through: number;
64
+ };
65
+ /** How a run ended. */
66
+ status?: "ok" | "stopped" | "error";
67
+ /** The host's own extras — a card id, a task step. */
68
+ vars?: Record<string, unknown>;
69
+ }
70
+ /** What one hook did. A runner returns one per hook it considered, in configuration order. */
71
+ export interface HookOutcome {
72
+ /** Whatever the hook belongs to — for the pool, the server row. Together with `hookId`, unique. */
73
+ serverId: string;
74
+ /** What an injected block and a note call it. */
75
+ label: string;
76
+ hookId: string;
77
+ event: HookEvent;
78
+ /** The hook ran and did not report an error. */
79
+ ok: boolean;
80
+ /** What it returned. Absent when it returned nothing, and when it failed. */
81
+ text?: string;
82
+ /** Why it failed or was skipped. */
83
+ error?: string;
84
+ /** Set when the hook never ran. */
85
+ skipped?: boolean;
86
+ /** Wall time, dispatch to answer. */
87
+ ms: number;
88
+ /** Hand `text` to the model. Only honoured on `INJECT_EVENTS`. */
89
+ inject: boolean;
90
+ /** The most of `text` that is injected, in estimated tokens. */
91
+ maxTokens: number;
92
+ }
93
+ /**
94
+ * One hook's line for whoever is watching: the context it added, or why it added none. A hook
95
+ * that worked and added nothing gets no note — a remember that succeeded is not news.
96
+ */
97
+ export interface HookNote {
98
+ event: HookEvent;
99
+ /** The outcome's `label`. */
100
+ source: string;
101
+ hookId: string;
102
+ /** Estimated tokens of context it added to the request. */
103
+ tokens?: number;
104
+ /** The context it added, as the model read it: after the cap, without the `<context>` tags. */
105
+ text?: string;
106
+ /** Why it added nothing: it failed, timed out, or never ran. */
107
+ error?: string;
108
+ }
109
+ /**
110
+ * Runs one event's hooks. Should resolve rather than reject — a hook failing is an outcome — but
111
+ * `gather` and `notify` survive one that does not.
112
+ *
113
+ * The pool's `runHooks` fits as it is; wrap it to pass its scope or `onNotice`.
114
+ */
115
+ export type HookRunner = (event: HookEvent, context: HookContext, options: {
116
+ signal?: AbortSignal;
117
+ }) => Promise<readonly HookOutcome[]>;
118
+ /** The context a set of outcomes adds to a request, and a note for each hook worth mentioning. */
119
+ export interface Gathered {
120
+ /** The `<context>` blocks, blank-line separated, or empty when no hook added anything. */
121
+ context: string;
122
+ notes: HookNote[];
123
+ }
124
+ /**
125
+ * The most context all of a request's hooks add between them by default, in estimated tokens.
126
+ *
127
+ * Enough for a handful of recalled memories, and small against any window worth running an agent
128
+ * in. The point is that a generous hook cannot crowd out the conversation it was meant to inform.
129
+ * `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
130
+ */
131
+ export declare const HOOK_CONTEXT_TOKENS = 2000;
132
+ /** What hooks are held to across a process. Every field optional; see `configureHooks`. */
133
+ export interface HookOptions {
134
+ /**
135
+ * The budget every injecting hook shares, when a call does not give its own. Each hook is still
136
+ * held to its own `maxTokens` inside it.
137
+ */
138
+ contextTokens?: number;
139
+ }
140
+ /**
141
+ * Changes what hooks are held to, for a process whose windows are not the size these defaults
142
+ * were chosen for.
143
+ *
144
+ * Module-level for the same reason `configureEvents` is: a budget is a deployment's setting, said
145
+ * once at startup. A caller that sizes it per model or per agent — a 128k window can afford more
146
+ * recall than an 8k one — passes `maxTokens` to `gather` instead, which wins over this.
147
+ *
148
+ * @param options The limits to change. A field left out — or given anything that is not a number
149
+ * above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
150
+ * above zero, and lifts the shared budget entirely.
151
+ * @returns Everything in force afterwards, including what this call did not change.
152
+ */
153
+ export declare function configureHooks(options?: HookOptions): Required<HookOptions>;
154
+ /**
155
+ * Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
156
+ * `resetAll` calls it.
157
+ */
158
+ export declare const resetHooks: () => void;
159
+ /**
160
+ * Said once, above the blocks, so the model reads them as background rather than instructions.
161
+ * Names no host; `withContext` takes another for one that wants to.
162
+ */
163
+ export declare const HOOK_PREFACE: string;
164
+ /**
165
+ * Builds the context a set of outcomes adds and the notes that go with it.
166
+ *
167
+ * Each injected outcome is wrapped in `<context source="…">` naming its label, so a model reading
168
+ * a recalled line can tell it is a memory rather than something the user said. Each is held to
169
+ * its own `maxTokens` and the whole to `maxTokens` here, and a block past the total is dropped
170
+ * whole rather than cut to a stub. For any hooks the pool's `validateHooks` accepts, the text is
171
+ * what its `contextBlocks` builds from the same outcomes, character for character, so a host
172
+ * moving between the two sends the same request.
173
+ *
174
+ * The note keeps each hook's text as it was cut, so a host can show exactly what the model was
175
+ * given without re-deriving the caps or parsing the wrapper back off.
176
+ *
177
+ * @param outcomes What the runners returned. An injecting outcome on an event that cannot inject
178
+ * adds nothing; a failed one is noted wherever it falls, including past the budget.
179
+ * @param maxTokens The budget every block shares. Absent, or not a number above zero, is what
180
+ * `configureHooks` last set — `HOOK_CONTEXT_TOKENS` unless something moved it.
181
+ */
182
+ export declare function assembleContext(outcomes: readonly HookOutcome[], maxTokens?: number): Gathered;
183
+ /**
184
+ * The request, with the hooks' context added to this turn's question.
185
+ *
186
+ * It goes on the question and not in the system prompt, because it is about the question — and a
187
+ * system prompt that changed every turn would miss the prompt cache every turn. Nothing is written
188
+ * back: a host that stores what the user typed never remembers the context as something they said.
189
+ *
190
+ * @param history The request's messages. Neither the array nor any message in it is changed.
191
+ * @param index Where this turn's question sits in `history` — which is not where it sits in the
192
+ * session once a compaction has folded the head into a summary. Anything but a user message there
193
+ * leaves the request as it was.
194
+ * @param context What `assembleContext` built. Empty returns `history` itself.
195
+ * @param preface Said above the blocks. Defaults to `HOOK_PREFACE`.
196
+ * @returns `history` when there was nothing to add or nowhere to add it, otherwise a new array.
197
+ */
198
+ export declare function withContext(history: OpenAI.ChatCompletionMessageParam[], index: number, context: string, preface?: string): OpenAI.ChatCompletionMessageParam[];
199
+ /**
200
+ * A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing
201
+ * else.
202
+ *
203
+ * Tool calls and their results are left out. They are the model's working rather than the
204
+ * conversation, and most of a transcript's characters; a memory server that filed them would
205
+ * recall a directory listing ahead of the decision it led to.
206
+ *
207
+ * The uuid is the session, the position and a digest of what was said. Position alone is not
208
+ * stable — a retry cuts the transcript back and writes a new answer at the same index, and a
209
+ * server deduping on it would keep the answer that was thrown away — and text alone would make
210
+ * two identical "ok"s one memory. The same message sent twice, after its turn and again when it
211
+ * is compacted, is one.
212
+ *
213
+ * @param sessionId Prefixes every uuid, so two sessions never share one.
214
+ * @param messages The transcript, in whatever shape the host stores it, so long as each message
215
+ * has an OpenAI-style `role` and `content`.
216
+ * @param from The first index, inclusive. Below zero reads from the start.
217
+ * @param to The end, exclusive. Absent, or past the end, reads to the end.
218
+ */
219
+ export declare function turnMessages(sessionId: string, messages: readonly {
220
+ role: string;
221
+ content?: unknown;
222
+ }[], from: number, to?: number): HookMessage[];
223
+ /**
224
+ * Which turn of a session begins at a point, from 0: the user messages ahead of it.
225
+ *
226
+ * @param messages The transcript.
227
+ * @param before Where the turn begins. Absent is the end, which is the index of a turn whose
228
+ * question has not been appended yet.
229
+ */
230
+ export declare const turnIndex: (messages: readonly {
231
+ role: string;
232
+ }[], before?: number) => number;
233
+ /**
234
+ * Runs the hooks ahead of a request and builds what they add to it.
235
+ *
236
+ * This is on the path of the first token, so the events run together rather than one after the
237
+ * other, and a hook that fails costs the turn its context and never the turn — a runner that
238
+ * rejects outright is noted once for its event, with an empty `hookId` and `source`, and the rest
239
+ * go ahead. Bounding each hook's time is the runner's job; `signal` is how the turn ends all of
240
+ * them.
241
+ *
242
+ * @param run Runs one event's hooks.
243
+ * @param events Which to run: `["beforeTurn"]` ordinarily, and `sessionStart` ahead of it on a
244
+ * session's first turn. The outcomes are assembled in this order, so it is also the order the
245
+ * budget is spent in.
246
+ * @param context What the hooks are told.
247
+ * @param options `signal` is handed to the runner, and should be the turn's own: a user who
248
+ * stopped the turn stopped its recall. `onNote` hears each note as the whole is assembled.
249
+ * `maxTokens` is the shared budget for this request, read as `assembleContext` reads it: absent
250
+ * or unusable is the process's, from `configureHooks`.
251
+ */
252
+ export declare function gather(run: HookRunner, events: readonly HookEvent[], context: HookContext, { signal, onNote, maxTokens, }?: {
253
+ signal?: AbortSignal;
254
+ onNote?: (note: HookNote) => void;
255
+ maxTokens?: number;
256
+ }): Promise<Gathered>;
257
+ /**
258
+ * Runs the hooks for an event that reads what happened and adds nothing to a request. Never
259
+ * rejects, so a host can fire it without awaiting it.
260
+ *
261
+ * No signal: these run once the turn has been answered, and a reader who stops listening at that
262
+ * point has not asked for the turn not to be remembered.
263
+ *
264
+ * @param run Runs the event's hooks.
265
+ * @param event `afterTurn`, `beforeCompact`, `sessionEnd` or `sessionDelete`. An injecting event
266
+ * works too, but what its hooks return is dropped, since there is no request here to add it to.
267
+ * @param context What the hooks are told.
268
+ * @param onNote Hears each note — which, with nothing injected, is only ever a failure.
269
+ * @returns The same notes.
270
+ */
271
+ export declare function notify(run: HookRunner, event: HookEvent, context: HookContext, onNote?: (note: HookNote) => void): Promise<HookNote[]>;