@ixo/oracle-runtime-workers 0.18.0 → 0.20.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.
@@ -191,6 +191,7 @@ import {
191
191
  type RunsStatus,
192
192
  type ContextStatus,
193
193
  type SessionContextStatus,
194
+ type UserContextPrefetchStatus,
194
195
  type RunSummary,
195
196
  type TurnIdentity,
196
197
  type TurnRequest,
@@ -229,6 +230,14 @@ import {
229
230
  type HistoryMessage,
230
231
  type IndexableSession,
231
232
  } from '../memory/session-history-indexer';
233
+ import {
234
+ USER_CONTEXT_SLOTS,
235
+ UserContextFetcher,
236
+ type UserContextFetchOutcome,
237
+ type UserContextFetchParams,
238
+ } from '../memory/user-context-fetcher';
239
+ import { createUserContextInvalidationMiddleware } from '../memory/user-context-invalidation';
240
+ import { MEMORY_CAPABILITY } from '../plugins/delegated-capabilities';
232
241
  import {
233
242
  createSseSubscriberStream,
234
243
  runTurnFrames,
@@ -329,7 +338,7 @@ import {
329
338
  resolveTurnDelegation,
330
339
  WorkersUcanService,
331
340
  } from './ucan-service';
332
- import type { UcanDelegation } from '../plugin-api/types';
341
+ import type { UcanDelegation, UserContextData } from '../plugin-api/types';
333
342
 
334
343
  /** What one attempt's agent build reads from the request body. */
335
344
  type TurnBuildBody = Pick<
@@ -347,6 +356,56 @@ interface TurnBuildRun {
347
356
  continuation: string | null;
348
357
  /** Domain anchors the run started with (resumed attempts keep them). */
349
358
  domainPins?: DomainContextPins;
359
+ /** A `router.update` step for the run's client while the build waits. */
360
+ onProgress?: (step: string) => void;
361
+ }
362
+
363
+ /** Where the user-context prefetch runs: the engine and the `USER_CONTEXT_*` knobs. */
364
+ interface UserContextSettings {
365
+ engineUrl: string;
366
+ capMs?: number;
367
+ cacheTtlMs?: number;
368
+ }
369
+
370
+ /**
371
+ * The second half of a first-contact prefetch: waits for the gather (at
372
+ * most the cap; calling `onSlow` once it has waited past the grace) and
373
+ * returns the context the thread takes, or `undefined` when there is none
374
+ * — no answer within the cap, a failure, a skip, or the turn was aborted
375
+ * (the gather then keeps running in the background).
376
+ */
377
+ type UserContextSettle = (
378
+ onSlow: () => void,
379
+ signal: AbortSignal,
380
+ ) => Promise<UserContextData | undefined>;
381
+
382
+ /** How long a first turn waits on the user-context prefetch before saying so. */
383
+ const USER_CONTEXT_PROGRESS_GRACE_MS = 300;
384
+ const USER_CONTEXT_PROGRESS_STEP = 'Recalling your memories…';
385
+
386
+ /**
387
+ * A prefetched context with every slot present, a slot the engine did not
388
+ * return as an empty section (which renders nothing). The checkpointed
389
+ * value then marks a landed gather (see `hasUserContext`) even for a user
390
+ * with no memory yet, so the thread's later turns do not count as first
391
+ * contact; and the written value is the whole context under a merging
392
+ * reducer (the `MainAgentGraphState` annotation's) as well as under the
393
+ * last-value channel the agent declares (`userContextStateMiddleware`).
394
+ */
395
+ function withEverySlot(context: UserContextData): UserContextData {
396
+ const out: UserContextData = { ...context };
397
+ for (const slot of USER_CONTEXT_SLOTS) out[slot] ??= {};
398
+ return out;
399
+ }
400
+
401
+ /** A checkpointed `userContext` a gather has written (not the channel's `{}` default). */
402
+ function hasUserContext(value: unknown): boolean {
403
+ return (
404
+ typeof value === 'object' &&
405
+ value !== null &&
406
+ !Array.isArray(value) &&
407
+ Object.keys(value).length > 0
408
+ );
350
409
  }
351
410
 
352
411
  /**
@@ -877,6 +936,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
877
936
  /** Session-history → memory-engine indexing (built lazily, see `scheduleHistoryIndexing`). */
878
937
  private historyIndexer: SessionHistoryIndexer | null = null;
879
938
 
939
+ /** First-turn user-context prefetch (built lazily, see `startUserContextPrefetch`). */
940
+ private userContextFetcher: UserContextFetcher | null = null;
941
+
880
942
  /**
881
943
  * Transcripts captured BEFORE a session is deleted, keyed by session id:
882
944
  * the indexer runs after the RPC returned, when the rows are gone.
@@ -1734,7 +1796,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1734
1796
  if (!roomId)
1735
1797
  throw new ChannelError(
1736
1798
  428,
1737
- 'Companion room is not ready',
1799
+ "The user's oracle room is not ready",
1738
1800
  'room_not_ready',
1739
1801
  );
1740
1802
  return (await this.createSession(identity, { roomId, markerTxnId }))
@@ -1753,13 +1815,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1753
1815
  )
1754
1816
  throw new ChannelError(
1755
1817
  404,
1756
- 'Companion session not found',
1818
+ 'Oracle session not found',
1757
1819
  'session_not_found',
1758
1820
  );
1759
1821
  if (!(await this.roomIsEncrypted(roomId)))
1760
1822
  throw new ChannelError(
1761
1823
  409,
1762
- 'Companion room must be encrypted',
1824
+ "The user's oracle room must be encrypted",
1763
1825
  'room_not_encrypted',
1764
1826
  );
1765
1827
  },
@@ -2352,7 +2414,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2352
2414
  if (next !== null) deadlines.push(Math.max(next, now + 1000));
2353
2415
  }
2354
2416
 
2355
- // Channel requests: retry the Companion room mirrors that are due, and
2417
+ // Channel requests: retry the user's oracle room mirrors that are due, and
2356
2418
  // prune (bounded) receipts that never started a run and old run
2357
2419
  // tombstones (channels/turns.ts).
2358
2420
  if (this.channelTurns) {
@@ -3026,6 +3088,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3026
3088
  mintInvocation: (target, opts) =>
3027
3089
  ambient.ucan.mintInvocation(userDid, target, opts),
3028
3090
  },
3091
+ // Newly indexed history changes what the prefetch would recall.
3092
+ onIndexed: (roomId) => this.userContextFetcher?.invalidate(roomId),
3029
3093
  speakerLabels: async (roomId) => {
3030
3094
  const prefs = await this.preferences
3031
3095
  ?.get(roomId)
@@ -3058,6 +3122,195 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3058
3122
  );
3059
3123
  }
3060
3124
 
3125
+ /**
3126
+ * The user-context prefetch's settings, or null where it does not run:
3127
+ * switched off (`USER_CONTEXT_PREFETCH=off`) or no memory engine.
3128
+ */
3129
+ private userContextSettings(): UserContextSettings | null {
3130
+ const env = this.core.validatedEnv;
3131
+ if (env['USER_CONTEXT_PREFETCH'] === 'off') return null;
3132
+ const engineUrl = env['MEMORY_ENGINE_URL'];
3133
+ if (typeof engineUrl !== 'string' || engineUrl.length === 0) return null;
3134
+ const capMs = env['USER_CONTEXT_PREFETCH_CAP_MS'];
3135
+ const cacheTtlMs = env['USER_CONTEXT_CACHE_TTL_MS'];
3136
+ return {
3137
+ engineUrl,
3138
+ ...(typeof capMs === 'number' ? { capMs } : {}),
3139
+ ...(typeof cacheTtlMs === 'number' ? { cacheTtlMs } : {}),
3140
+ };
3141
+ }
3142
+
3143
+ /** One fetcher per object: its cache outlives turns, sessions and resets. */
3144
+ private userContextFetcherFor(
3145
+ settings: UserContextSettings,
3146
+ ): UserContextFetcher {
3147
+ this.userContextFetcher ??= new UserContextFetcher(
3148
+ {
3149
+ memoryEngineUrl: settings.engineUrl,
3150
+ oracleDid: this.env.ORACLE_DID,
3151
+ // Looked up per call, not captured: a working-copy reset replaces
3152
+ // the ambient services while a gather may still be running.
3153
+ ucan: {
3154
+ hasSigningKey: () => this.ambient?.ucan.hasSigningKey() ?? false,
3155
+ resolveServiceDid: async (url) =>
3156
+ (await this.ambient?.ucan.resolveServiceDid(url)) ?? null,
3157
+ mintInvocation: async (target, opts) => {
3158
+ const ambient = this.ambient;
3159
+ const userDid = this.userDid;
3160
+ if (!ambient || !userDid)
3161
+ throw new Error('the user object is not booted');
3162
+ return ambient.ucan.mintInvocation(userDid, target, opts);
3163
+ },
3164
+ },
3165
+ logger: console,
3166
+ },
3167
+ {
3168
+ ...(settings.capMs !== undefined
3169
+ ? { blockingCapMs: settings.capMs }
3170
+ : {}),
3171
+ ...(settings.cacheTtlMs !== undefined
3172
+ ? { cacheTtlMs: settings.cacheTtlMs }
3173
+ : {}),
3174
+ },
3175
+ );
3176
+ return this.userContextFetcher;
3177
+ }
3178
+
3179
+ /**
3180
+ * Start a turn's user-context prefetch — stale-while-revalidate, as the
3181
+ * Node AgentBuilder does it. A thread's context is fixed on first
3182
+ * contact: a thread whose checkpoint already holds a gathered context
3183
+ * never waits and never takes a newer answer; it only refreshes the
3184
+ * room's cache in the background (a no-op while the cache is fresh), so
3185
+ * the NEXT session in the room starts from it. First contact — including
3186
+ * a thread whose earlier first turn got nothing (timeout, failure, skip)
3187
+ * — waits for the gather, at most the cap. Returns the step the build
3188
+ * awaits right before the agent is built, or null where this turn
3189
+ * writes no context. Nothing here throws; every gather is handed to
3190
+ * `ctx.waitUntil`, so one that outlives the cap still fills the cache.
3191
+ */
3192
+ private startUserContextPrefetch(input: {
3193
+ sessionId: string;
3194
+ roomId: string | undefined;
3195
+ userDid: string;
3196
+ delegation: UcanDelegation;
3197
+ prior: unknown;
3198
+ client: TurnRequest['client'];
3199
+ roomKind: TurnRequest['roomKind'];
3200
+ }): UserContextSettle | null {
3201
+ // Task runs never get the user's memory (see `scheduleHistoryIndexing`).
3202
+ if (!input.roomId || input.sessionId.startsWith(TASK_SESSION_PREFIX))
3203
+ return null;
3204
+ const settings = this.userContextSettings();
3205
+ const ambient = this.ambient;
3206
+ if (!settings || !ambient) return null;
3207
+ // A group room's reply is posted to every member: one user's memory
3208
+ // must not be (as direct reads are not, see `admissionApplies`).
3209
+ if (input.client === 'matrix' && input.roomKind === 'group') {
3210
+ console.log('[user-context] skipped: group room');
3211
+ return null;
3212
+ }
3213
+ // Without a memory grant the gather could only fail at the mint: skip
3214
+ // it before any request (the engine's did.json included).
3215
+ if (
3216
+ !ambient.ucan.hasCapability(
3217
+ input.delegation,
3218
+ MEMORY_CAPABILITY.resource,
3219
+ MEMORY_CAPABILITY.action,
3220
+ )
3221
+ ) {
3222
+ console.log(
3223
+ `[user-context] skipped: the delegation grants no ${MEMORY_CAPABILITY.resource} ${MEMORY_CAPABILITY.action}`,
3224
+ );
3225
+ return null;
3226
+ }
3227
+ const fetcher = this.userContextFetcherFor(settings);
3228
+ const params: UserContextFetchParams = {
3229
+ roomId: input.roomId,
3230
+ userDid: input.userDid,
3231
+ sessionId: input.sessionId,
3232
+ };
3233
+ const keepGathering = (): void =>
3234
+ this.ctx.waitUntil(
3235
+ fetcher.refresh(params).catch((err: unknown) => {
3236
+ console.warn(
3237
+ `[user-context] background refresh failed: ${err instanceof Error ? err.message : String(err)}`,
3238
+ );
3239
+ }),
3240
+ );
3241
+ if (hasUserContext(input.prior)) {
3242
+ keepGathering();
3243
+ return null;
3244
+ }
3245
+ const pending = fetcher
3246
+ .fetch(params)
3247
+ .catch((err: unknown): UserContextFetchOutcome => {
3248
+ const reason = err instanceof Error ? err.message : String(err);
3249
+ console.warn(`[user-context] failed: ${reason}`);
3250
+ return { kind: 'failed', reason };
3251
+ });
3252
+ // Joins the gather `fetch` started (a no-op on a cache hit).
3253
+ keepGathering();
3254
+ return async (onSlow, signal) => {
3255
+ if (signal.aborted) return undefined;
3256
+ const timer = setTimeout(onSlow, USER_CONTEXT_PROGRESS_GRACE_MS);
3257
+ let onAbort: (() => void) | undefined;
3258
+ const aborted = new Promise<undefined>((resolve) => {
3259
+ onAbort = () => resolve(undefined);
3260
+ signal.addEventListener('abort', onAbort, { once: true });
3261
+ });
3262
+ try {
3263
+ const outcome = await Promise.race([pending, aborted]);
3264
+ return outcome?.kind === 'cached' || outcome?.kind === 'fresh'
3265
+ ? withEverySlot(outcome.context)
3266
+ : undefined;
3267
+ } finally {
3268
+ clearTimeout(timer);
3269
+ if (onAbort) signal.removeEventListener('abort', onAbort);
3270
+ }
3271
+ };
3272
+ }
3273
+
3274
+ /** `GET /debug/context` — the prefetch for a session's room, else the user's room. */
3275
+ private async userContextPrefetchStatus(
3276
+ sessionId: string | undefined,
3277
+ ): Promise<UserContextPrefetchStatus> {
3278
+ const userDid = this.userDid;
3279
+ const sessionRoom = sessionId
3280
+ ? (this.sessionRooms.get(sessionId) ??
3281
+ (await this.sessions?.getSession(sessionId))?.roomId)
3282
+ : undefined;
3283
+ const room =
3284
+ sessionRoom ??
3285
+ this.mainRoomId ??
3286
+ (userDid
3287
+ ? (await this.gateway.resolveUserRoom(userDid).catch(() => null))
3288
+ ?.roomId
3289
+ : undefined);
3290
+ const status =
3291
+ room && this.userContextFetcher
3292
+ ? this.userContextFetcher.status(room)
3293
+ : undefined;
3294
+ const last = status?.lastOutcome;
3295
+ return {
3296
+ enabled: this.userContextSettings() !== null,
3297
+ ...(room ? { room } : {}),
3298
+ cached: status?.cached ?? false,
3299
+ ...(status?.ageMs !== undefined ? { ageMs: status.ageMs } : {}),
3300
+ inFlight: status?.inFlight ?? false,
3301
+ ...(last ? { lastOutcome: last.kind } : {}),
3302
+ ...(last?.kind === 'failed' || last?.kind === 'skipped'
3303
+ ? { lastReason: last.reason }
3304
+ : {}),
3305
+ ...(last?.kind === 'fresh'
3306
+ ? { lastMs: last.ms, lastSlots: last.slots }
3307
+ : {}),
3308
+ ...(status?.lastAt !== undefined
3309
+ ? { lastAt: new Date(status.lastAt).toISOString() }
3310
+ : {}),
3311
+ };
3312
+ }
3313
+
3061
3314
  async listMessages(
3062
3315
  identity: TurnIdentity,
3063
3316
  sessionId: string,
@@ -4454,6 +4707,21 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4454
4707
  resumed: !freshInput,
4455
4708
  continuation: freshInput ? null : live.continuation,
4456
4709
  ...(stored.domainPins ? { domainPins: stored.domainPins } : {}),
4710
+ // The Portal shows `router.update` steps (as it shows "Queued…");
4711
+ // chat surfaces have no place for one.
4712
+ ...(req.client === 'portal'
4713
+ ? {
4714
+ onProgress: (step: string) => {
4715
+ if (live.buffer.isClosed) return;
4716
+ live.buffer.push('router.update', {
4717
+ step,
4718
+ sessionId: req.sessionId,
4719
+ requestId: req.requestId,
4720
+ runId: live.runId,
4721
+ });
4722
+ },
4723
+ }
4724
+ : {}),
4457
4725
  },
4458
4726
  );
4459
4727
  const sessionId = req.sessionId;
@@ -4763,7 +5031,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4763
5031
  outcome: RunOutcome,
4764
5032
  ): Promise<void> {
4765
5033
  // A channel run's reply (or the end of a run without one) is mirrored
4766
- // into the Companion room in the background: never awaited here, so
5034
+ // into the user's oracle room in the background: never awaited here, so
4767
5035
  // the session's next queued run starts at once. Without the handle (a
4768
5036
  // reset dropped it mid-run) the next poll or alarm mirrors it.
4769
5037
  if (record.client === 'channel') this.channelTurns?.deliverReply();
@@ -4845,6 +5113,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4845
5113
  ...(sessionId
4846
5114
  ? { session: await this.sessionContextStatus(sessionId) }
4847
5115
  : {}),
5116
+ userContext: await this.userContextPrefetchStatus(sessionId),
4848
5117
  };
4849
5118
  }
4850
5119
 
@@ -5345,6 +5614,29 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5345
5614
  }
5346
5615
  if (sessionRoomId) this.sessionRooms.set(req.sessionId, sessionRoomId);
5347
5616
 
5617
+ // The delegation the whole turn reads: the prefetch's memory grant
5618
+ // here, the router's candidates and every capability check of the
5619
+ // build and its tools below.
5620
+ const ucanDelegation = await this.turnDelegation(req.identity);
5621
+ const existing = await saver.getTupleWithoutMessages({
5622
+ configurable: { thread_id: req.sessionId },
5623
+ });
5624
+ const priorState = existing?.checkpoint.channel_values ?? {};
5625
+ // What the memory engine knows about the user, for the prompt's
5626
+ // context block: started now so the round-trip overlaps the rest of
5627
+ // the build, settled right before the agent is built.
5628
+ const settleUserContext = suppliedContextOnly
5629
+ ? null
5630
+ : this.startUserContextPrefetch({
5631
+ sessionId: req.sessionId,
5632
+ roomId: sessionRoomId,
5633
+ userDid: req.identity.userDid,
5634
+ delegation: ucanDelegation,
5635
+ prior: priorState.userContext,
5636
+ client: req.client,
5637
+ roomKind: req.roomKind,
5638
+ });
5639
+
5348
5640
  // A Matrix turn with no usable delegation: ask the web app to open its
5349
5641
  // "authorize for Matrix" modal (throttled per user, best-effort) — the
5350
5642
  // Node AgentBuilder's `maybePromptReauth`.
@@ -5425,10 +5717,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5425
5717
  signal: abortController.signal,
5426
5718
  });
5427
5719
 
5428
- const existing = await saver.getTupleWithoutMessages({
5429
- configurable: { thread_id: req.sessionId },
5430
- });
5431
- const priorState = existing?.checkpoint.channel_values ?? {};
5432
5720
  // Request metadata (editor room, space, session run, entity) → state,
5433
5721
  // by the Node agent-builder's rules (see turn-metadata.ts).
5434
5722
  const meta = parseTurnMetadata(
@@ -5461,9 +5749,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5461
5749
  client: req.client,
5462
5750
  env: langsmithEnvFromWorkerEnv(this.env),
5463
5751
  });
5464
- // The delegation the whole turn reads: the router's candidates here,
5465
- // every capability check of the build and its tools below.
5466
- const ucanDelegation = await this.turnDelegation(req.identity);
5467
5752
  const preloadedPlugins =
5468
5753
  !suppliedContextOnly && this.capabilityRouter
5469
5754
  ? await this.capabilityRouter({
@@ -5597,6 +5882,17 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5597
5882
  // which is a tool result to the main agent. Wrapped tools are capped
5598
5883
  // inside the wrapper (already under the cap by the time it runs).
5599
5884
  const capMiddleware = createResultCapMiddleware(resultCap);
5885
+ // A successful memory write drops the room's prefetched context, so
5886
+ // the next session gathers again (this thread keeps its own).
5887
+ const userContextInvalidation =
5888
+ sessionRoomId && this.userContextSettings()
5889
+ ? createUserContextInvalidationMiddleware({
5890
+ roomId: sessionRoomId,
5891
+ invalidate: (roomId) =>
5892
+ this.userContextFetcher?.invalidate(roomId),
5893
+ logger: console,
5894
+ })
5895
+ : null;
5600
5896
 
5601
5897
  // Chat delivery: a channel or Matrix turn replies as short messages and
5602
5898
  // may hand long content to `create_artifact` (bound only when artefact
@@ -5623,6 +5919,16 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5623
5919
  ]
5624
5920
  : [];
5625
5921
 
5922
+ // The prefetched user context on first contact (bounded by the cap and
5923
+ // the turn's abort); a wait past the grace tells a Portal client what
5924
+ // it waits on.
5925
+ const userContext = settleUserContext
5926
+ ? await settleUserContext(
5927
+ () => run.onProgress?.(USER_CONTEXT_PROGRESS_STEP),
5928
+ abortController.signal,
5929
+ )
5930
+ : undefined;
5931
+
5626
5932
  const built = await createMainAgent({
5627
5933
  executionProfile: req.executionProfile,
5628
5934
  registries: core.registries,
@@ -5660,7 +5966,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5660
5966
  : [],
5661
5967
  // Applied to the main agent AND every sub-agent: a sub-agent's
5662
5968
  // own tool calls are marked and capped too.
5663
- toolMiddlewares: [marksMiddleware, capMiddleware],
5969
+ toolMiddlewares: [
5970
+ marksMiddleware,
5971
+ capMiddleware,
5972
+ ...(userContextInvalidation ? [userContextInvalidation] : []),
5973
+ ],
5664
5974
  // Budget, scheduling and write claims, innermost around each tool
5665
5975
  // call (main agent and sub-agents alike).
5666
5976
  toolExecution: executionMiddleware,
@@ -5708,6 +6018,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5708
6018
  state: {
5709
6019
  ...(suppliedContextOnly ? {} : priorState),
5710
6020
  userPreferences,
6021
+ ...(userContext ? { userContext } : {}),
5711
6022
  ...metadataBuildState(meta, priorMeta),
5712
6023
  // The client-declared surface: the portal and AG-UI plugins turn
5713
6024
  // these into tools at build time (`getRequestTools` reads
@@ -5791,6 +6102,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5791
6102
  ],
5792
6103
  config: { did: req.identity.userDid },
5793
6104
  client: req.client,
6105
+ // First contact's gathered context is checkpointed with the turn,
6106
+ // every slot present; later turns write nothing and keep it.
6107
+ ...(userContext ? { userContext } : {}),
5794
6108
  ...metadataGraphInput(meta, priorMeta),
5795
6109
  ...surface.input,
5796
6110
  };
@@ -68,6 +68,8 @@ export interface SessionHistoryIndexerDeps {
68
68
  ): Promise<string>;
69
69
  };
70
70
  speakerLabels(roomId: string): Promise<SpeakerLabels>;
71
+ /** Called after the engine accepted an upload for the room. */
72
+ onIndexed?(roomId: string): void;
71
73
  logger: Logger;
72
74
  fetchImpl?: typeof fetch;
73
75
  /** Test hooks. */
@@ -316,6 +318,7 @@ export class SessionHistoryIndexer {
316
318
  `memory engine rejected the conversation (${res.status}): ${text.slice(0, 200)}`,
317
319
  );
318
320
  }
321
+ deps.onIndexed?.(roomId);
319
322
  }
320
323
 
321
324
  const newCount = lastProcessedCount + newMessages.length;