@ixo/oracle-runtime-workers 0.24.0 → 0.26.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.
@@ -39,6 +39,11 @@ import type {
39
39
  import { ChannelTurns } from '../channels/turns';
40
40
  import { HubActionClient } from '../hub-actions/client';
41
41
  import { prepareHubAction } from '../hub-actions/request';
42
+ import {
43
+ bankReceiptSummary,
44
+ describeAmount,
45
+ toBaseUnits,
46
+ } from '../hub-actions/transfer';
42
47
  import {
43
48
  HUB_CHECK_AFTER_EXPIRY_MS,
44
49
  HubActionRefused,
@@ -49,6 +54,9 @@ import {
49
54
  canonicalJson,
50
55
  type ActionCapabilitiesV1,
51
56
  type HubActionOptionsV2,
57
+ type HubActionOptionsV3,
58
+ parseOperation,
59
+ v1Receipt,
52
60
  type ActionOperationV1,
53
61
  type ActionOutcomeV1,
54
62
  } from '@ixo/action-protocol';
@@ -103,16 +111,17 @@ import {
103
111
  applyAttachmentRetention,
104
112
  ATTACHMENT_PAYLOAD_TURNS,
105
113
  ATTACHMENT_PLACEHOLDER_PREFIX,
106
- attachmentMetas,
107
- attachmentRef,
114
+ createAttachmentViewSurface,
115
+ findPersistedAttachmentMeta,
116
+ IngestTextLru,
108
117
  INLINE_PAYLOAD_NEEDLE,
109
118
  isAttachmentViewMessage,
110
119
  offloadInlinePayloads,
111
120
  parseAttachmentInputs,
112
121
  prepareAttachments,
113
- viewAttachment,
114
122
  type AttachmentViewSurface,
115
123
  type MatrixMediaSource,
124
+ type ViewAttachmentDeps,
116
125
  type SandboxUploadConfig,
117
126
  DOWNLOAD_TIMEOUT_MS,
118
127
  MAX_FILE_SIZE,
@@ -191,6 +200,7 @@ import {
191
200
  } from '../sqlite/cache-config';
192
201
  import { DoSqliteDatabase, type DoSqliteOpenOptions } from '../sqlite/database';
193
202
  import type { TierFlushResult } from '../sqlite/do-vfs';
203
+ import type { AttachmentMeta } from '../sqlite/serialization';
194
204
  import {
195
205
  DEFAULT_TIER_HOT_BUDGET_BYTES,
196
206
  type PageTierOptions,
@@ -264,6 +274,7 @@ import { decideBootDirty } from './boot-dirty';
264
274
  import { decideFlush } from './flush-schedule';
265
275
  import { mirrorTxnId, mirrorEventContent, RoomMirror } from './room-mirror';
266
276
  import { prefixSpeaker } from './speaker-prefix';
277
+ import { assembleTurnMessages } from './turn-messages';
267
278
  import {
268
279
  type ObservedMessage,
269
280
  summarizeObservedMessages,
@@ -599,6 +610,25 @@ const ACTION_TICK_BUDGET_MS = 15_000;
599
610
  const ACTION_PRUNE_INTERVAL_MS = 60 * 60_000;
600
611
  /** How long the Hub's capabilities answer is reused per object. */
601
612
  const HUB_CAPABILITIES_CACHE_MS = 60_000;
613
+ /** A Hub network name as the chain-id config key it is configured under. */
614
+ /**
615
+ * Whether two Hub outcomes are the same event. Receipts are compared in the
616
+ * 0.4.1 shape: an action prepared at protocol version 2 (runtime 0.23.0) gets
617
+ * its callback without the 0.5.0 bank fields (`denom`, `amountBaseUnits`),
618
+ * while the confirming status read (version 3) carries them.
619
+ */
620
+ function sameHubOutcome(a: ActionOutcomeV1, b: ActionOutcomeV1): boolean {
621
+ return (
622
+ canonicalJson({ ...a, receipt: v1Receipt(a.receipt) }) ===
623
+ canonicalJson({ ...b, receipt: v1Receipt(b.receipt) })
624
+ );
625
+ }
626
+
627
+ const HUB_NETWORK_CHAIN = {
628
+ 'ixo mainnet': 'mainnet',
629
+ 'ixo testnet': 'testnet',
630
+ 'ixo devnet': 'devnet',
631
+ } as const;
602
632
  /** How long a `hub_action_options` answer vouches for a Gmail connection. */
603
633
  const HUB_OPTIONS_REUSE_MS = 10 * 60_000;
604
634
  /** A channel poll waits this long for Auth Hub, once per poll at most. */
@@ -1002,8 +1032,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1002
1032
  at: number;
1003
1033
  } | null = null;
1004
1034
  /** The last `hub_action_options` answer for this owner (Gmail connection checks). */
1005
- private lastHubOptions: { value: HubActionOptionsV2; at: number } | null =
1006
- null;
1035
+ private lastHubOptions: {
1036
+ value: HubActionOptionsV2 | HubActionOptionsV3;
1037
+ at: number;
1038
+ } | null = null;
1007
1039
  private topicProposals: TopicProposalStore | null = null;
1008
1040
  /** `assertTopicDependenciesMemo`: card id and digest -> when it passed. */
1009
1041
  private topicAuthorityMemo: Map<string, number> | undefined;
@@ -1011,6 +1043,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1011
1043
  private lastActionPruneAt: number | null = null;
1012
1044
  /** Text `view_attachment` extracted, per session (`attachments/view-cache.ts`). */
1013
1045
  private attachmentTextCache: AttachmentTextCacheStore | null = null;
1046
+ /** Large texts of document ingestion, in memory (a few, minutes). */
1047
+ private readonly ingestTextLru = new IngestTextLru();
1014
1048
  /**
1015
1049
  * Per-model context windows; learned limits persist in this object's KV
1016
1050
  * storage (`ctxwin:<model>`), so a provider's rejection is remembered
@@ -2187,8 +2221,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2187
2221
  requestKey: string;
2188
2222
  operation: ActionOperationV1;
2189
2223
  }) => this.requestHubAction(input),
2224
+ transfer: (input) => this.transferHubAction(input),
2190
2225
  status: (ownerDid: string, actionId: string) =>
2191
- this.statusHubAction(ownerDid, actionId),
2226
+ this.hubActionStatusForTool(ownerDid, actionId),
2192
2227
  },
2193
2228
  }
2194
2229
  : {}),
@@ -3207,11 +3242,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3207
3242
  this.markDirty();
3208
3243
  await this.requestHousekeeping(Date.now() + 1000);
3209
3244
  }
3210
- const hubActionOutcomes = await this.hubOutcomesForRun(
3211
- ownerDid,
3212
- result.sessionId,
3213
- result.runId,
3214
- );
3245
+ // The gateway parses with @ixo/action-protocol 0.4.1, whose bank
3246
+ // receipt is exact: the 0.5.0 `denom`/`amountBaseUnits` fields are
3247
+ // dropped here (the owner-facing amount line comes from
3248
+ // hub_action_status instead), so the emitted shape stays 0.4.1.
3249
+ const hubActionOutcomes = (
3250
+ await this.hubOutcomesForRun(ownerDid, result.sessionId, result.runId)
3251
+ ).map((outcome) => ({ ...outcome, receipt: v1Receipt(outcome.receipt) }));
3215
3252
  return {
3216
3253
  ...result,
3217
3254
  ...(actionOutcomes.length ? { actionOutcomes } : {}),
@@ -3219,14 +3256,64 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3219
3256
  };
3220
3257
  }
3221
3258
 
3222
- private async hubActionOptions(input: {
3259
+ /** `hub_action_options` and the transfer path: version 3, with the owner's tokens. */
3260
+ private async hubActionOptions(
3261
+ input: {
3262
+ ownerDid: string;
3263
+ provider: string;
3264
+ bindingId: string;
3265
+ bindingRevision: number;
3266
+ },
3267
+ bindingChecked = false,
3268
+ ): Promise<HubActionOptionsV3> {
3269
+ const client = this.hubActionClient;
3270
+ if (!client || input.ownerDid !== this.userDid)
3271
+ throw new Error('Hub action options are unavailable for this owner');
3272
+ return this.readHubOptions(input, bindingChecked, (binding) =>
3273
+ client.options(binding),
3274
+ );
3275
+ }
3276
+
3277
+ /**
3278
+ * Read the owner's options for the trusted binding (validated here
3279
+ * unless the caller already validated it in this same call) and keep
3280
+ * the read for the Gmail connection check.
3281
+ */
3282
+ private async readHubOptions<
3283
+ T extends HubActionOptionsV2 | HubActionOptionsV3,
3284
+ >(
3285
+ input: {
3286
+ ownerDid: string;
3287
+ provider: string;
3288
+ bindingId: string;
3289
+ bindingRevision: number;
3290
+ },
3291
+ bindingChecked: boolean,
3292
+ read: (binding: {
3293
+ bindingId: string;
3294
+ bindingRevision: number;
3295
+ }) => Promise<T>,
3296
+ ): Promise<T> {
3297
+ if (!bindingChecked) await this.assertHubBinding(input);
3298
+ const options = await read(input);
3299
+ if (
3300
+ options.ownerDid !== input.ownerDid ||
3301
+ options.bindingId !== input.bindingId ||
3302
+ options.bindingRevision !== input.bindingRevision
3303
+ )
3304
+ throw new Error(
3305
+ 'Hub action options do not match this owner and binding',
3306
+ );
3307
+ this.lastHubOptions = { value: options, at: Date.now() };
3308
+ return options;
3309
+ }
3310
+
3311
+ private async assertHubBinding(input: {
3223
3312
  ownerDid: string;
3224
3313
  provider: string;
3225
3314
  bindingId: string;
3226
3315
  bindingRevision: number;
3227
- }) {
3228
- if (!this.hubActionClient || input.ownerDid !== this.userDid)
3229
- throw new Error('Hub action options are unavailable for this owner');
3316
+ }): Promise<void> {
3230
3317
  await assertActiveChannelBinding(
3231
3318
  {
3232
3319
  userDid: input.ownerDid,
@@ -3238,17 +3325,34 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3238
3325
  },
3239
3326
  this.env,
3240
3327
  );
3241
- const options = await this.hubActionClient.options(input);
3328
+ }
3329
+
3330
+ /**
3331
+ * Only a run an inbound channel message started may prepare a review:
3332
+ * a scheduled or background run has no user asking for it. Checked
3333
+ * before any Hub read.
3334
+ */
3335
+ private async assertHubTurnOrigin(input: {
3336
+ ownerDid: string;
3337
+ sessionId: string;
3338
+ runId: string;
3339
+ }): Promise<void> {
3340
+ const record = await this.runStore?.get(input.runId);
3341
+ const turn = record
3342
+ ? (JSON.parse(record.request) as StoredRunRequest).turn
3343
+ : null;
3242
3344
  if (
3243
- options.ownerDid !== input.ownerDid ||
3244
- options.bindingId !== input.bindingId ||
3245
- options.bindingRevision !== input.bindingRevision
3345
+ !turn ||
3346
+ turn.client !== 'channel' ||
3347
+ !turn.channel?.remoteMessageRef ||
3348
+ turn.identity.userDid !== input.ownerDid ||
3349
+ turn.sessionId !== input.sessionId ||
3350
+ turn.sessionId.startsWith(SYNTHETIC_SESSION_PREFIX)
3246
3351
  )
3247
- throw new Error(
3248
- 'Hub action options do not match this owner and binding',
3352
+ throw new HubActionRefused(
3353
+ 'limit',
3354
+ 'Reviews are prepared only in reply to a message from the user.',
3249
3355
  );
3250
- this.lastHubOptions = { value: options, at: Date.now() };
3251
- return options;
3252
3356
  }
3253
3357
 
3254
3358
  /**
@@ -3280,7 +3384,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3280
3384
  cached.value.bindingId === input.bindingId &&
3281
3385
  cached.value.bindingRevision === input.bindingRevision
3282
3386
  ? cached.value
3283
- : await this.hubActionOptions(input);
3387
+ : await this.readHubOptions(input, true, (binding) =>
3388
+ this.hubActionClient!.connectionOptions(binding),
3389
+ );
3284
3390
  if (
3285
3391
  !options.connections.some(
3286
3392
  (c) =>
@@ -3320,50 +3426,32 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3320
3426
  return value;
3321
3427
  }
3322
3428
 
3323
- private async requestHubAction(input: {
3324
- ownerDid: string;
3325
- sessionId: string;
3326
- runId: string;
3327
- requestId: string;
3328
- provider: string;
3329
- bindingId: string;
3330
- bindingRevision: number;
3331
- requestKey: string;
3332
- operation: ActionOperationV1;
3333
- }) {
3429
+ /**
3430
+ * `checked`: the transfer path already ran the turn-origin refusal and
3431
+ * validated the binding in this same call.
3432
+ */
3433
+ private async requestHubAction(
3434
+ input: {
3435
+ ownerDid: string;
3436
+ sessionId: string;
3437
+ runId: string;
3438
+ requestId: string;
3439
+ provider: string;
3440
+ bindingId: string;
3441
+ bindingRevision: number;
3442
+ requestKey: string;
3443
+ operation: ActionOperationV1;
3444
+ },
3445
+ checked = false,
3446
+ ) {
3334
3447
  const store = this.hubActionStore;
3335
3448
  const client = this.hubActionClient;
3336
3449
  if (!store || !client || input.ownerDid !== this.userDid)
3337
3450
  throw new Error('Hub actions are unavailable for this channel');
3338
- // Only a run an inbound channel message started may prepare a review:
3339
- // a scheduled or background run has no user asking for it.
3340
- const record = await this.runStore?.get(input.runId);
3341
- const turn = record
3342
- ? (JSON.parse(record.request) as StoredRunRequest).turn
3343
- : null;
3344
- if (
3345
- !turn ||
3346
- turn.client !== 'channel' ||
3347
- !turn.channel?.remoteMessageRef ||
3348
- turn.identity.userDid !== input.ownerDid ||
3349
- turn.sessionId !== input.sessionId ||
3350
- turn.sessionId.startsWith(SYNTHETIC_SESSION_PREFIX)
3351
- )
3352
- throw new HubActionRefused(
3353
- 'limit',
3354
- 'Reviews are prepared only in reply to a message from the user.',
3355
- );
3356
- await assertActiveChannelBinding(
3357
- {
3358
- userDid: input.ownerDid,
3359
- channel: {
3360
- provider: input.provider,
3361
- bindingId: input.bindingId,
3362
- bindingRevision: input.bindingRevision,
3363
- },
3364
- },
3365
- this.env,
3366
- );
3451
+ if (!checked) {
3452
+ await this.assertHubTurnOrigin(input);
3453
+ await this.assertHubBinding(input);
3454
+ }
3367
3455
  await this.assertGmailOperationAllowed(input);
3368
3456
  const reference = await prepareHubAction(
3369
3457
  {
@@ -3385,6 +3473,107 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3385
3473
  return reference;
3386
3474
  }
3387
3475
 
3476
+ /**
3477
+ * A bank send of one of the owner's listed tokens. The token comes from
3478
+ * a fresh options read (its balance must be current), the amount is
3479
+ * converted to base units with the listed decimals (unverified tokens
3480
+ * list 0: base units), refused when zero or above the balance; the
3481
+ * network is the account's capability, the chain id this Worker's own
3482
+ * for that network, the sender the owner's account. No fee is sent: the
3483
+ * Hub simulates gas and sets it.
3484
+ */
3485
+ private async transferHubAction(input: {
3486
+ ownerDid: string;
3487
+ sessionId: string;
3488
+ runId: string;
3489
+ requestId: string;
3490
+ provider: string;
3491
+ bindingId: string;
3492
+ bindingRevision: number;
3493
+ requestKey: string;
3494
+ toAddress: string;
3495
+ denom: string;
3496
+ amount: string;
3497
+ memo: string;
3498
+ }) {
3499
+ if (!this.hubActionStore || input.ownerDid !== this.userDid)
3500
+ throw new Error('Hub actions are unavailable for this channel');
3501
+ await this.assertHubTurnOrigin(input);
3502
+ await this.assertHubBinding(input);
3503
+ const options = await this.hubActionOptions(input, true);
3504
+ const tokens = options.transfer?.tokens;
3505
+ if (!tokens?.length)
3506
+ throw new HubActionRefused(
3507
+ 'refused',
3508
+ 'Bank sends are not available for this owner right now (no token list from Auth Hub).',
3509
+ );
3510
+ const token = tokens.find((item) => item.denom === input.denom);
3511
+ if (!token)
3512
+ throw new HubActionRefused(
3513
+ 'refused',
3514
+ `That token is not in the owner's list; use a denom exactly as hub_action_options lists it (${tokens
3515
+ .map((item) => item.denom)
3516
+ .slice(0, 10)
3517
+ .join(', ')}).`,
3518
+ );
3519
+ const amountBaseUnits = toBaseUnits(input.amount, token.decimals);
3520
+ if (amountBaseUnits > BigInt(token.balanceBaseUnits))
3521
+ throw new HubActionRefused(
3522
+ 'refused',
3523
+ `The owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than this transfer.`,
3524
+ );
3525
+ const capabilities = await this.cachedHubCapabilities();
3526
+ if (!capabilities.network)
3527
+ throw new HubActionRefused(
3528
+ 'refused',
3529
+ 'Bank sends are not enabled for this oracle account.',
3530
+ );
3531
+ const network = HUB_NETWORK_CHAIN[capabilities.network];
3532
+ const chainId = chainIdsFromConfig(chainIdConfigSchema.parse(this.env))[
3533
+ network
3534
+ ];
3535
+ let operation: ActionOperationV1;
3536
+ try {
3537
+ operation = parseOperation({
3538
+ kind: 'ixo.bank.send.v1',
3539
+ chainId,
3540
+ network: capabilities.network,
3541
+ fromAddress: input.ownerDid.replace(/^did:ixo:/, ''),
3542
+ toAddress: input.toAddress,
3543
+ amountBaseUnits: amountBaseUnits.toString(),
3544
+ denom: token.denom,
3545
+ memo: input.memo,
3546
+ });
3547
+ } catch (error) {
3548
+ // Only the protocol refusing the operation built from the model's
3549
+ // fields (an invalid recipient or memo) is the model's to fix.
3550
+ throw new HubActionRefused(
3551
+ 'refused',
3552
+ `The transfer is incomplete (${error instanceof Error ? error.message : String(error)}).`,
3553
+ );
3554
+ }
3555
+ const reference = await this.requestHubAction(
3556
+ { ...input, operation },
3557
+ true,
3558
+ );
3559
+ await this.hubActionStore.setToken(reference.id, token);
3560
+ return reference;
3561
+ }
3562
+
3563
+ /**
3564
+ * `hub_action_status`: the outcome plus, for a bank send, the
3565
+ * owner-facing line (amount from the token stored at preparation).
3566
+ */
3567
+ private async hubActionStatusForTool(
3568
+ ownerDid: string,
3569
+ actionId: string,
3570
+ ): Promise<{ outcome: ActionOutcomeV1 | null; summary: string | null }> {
3571
+ const outcome = await this.statusHubAction(ownerDid, actionId);
3572
+ if (!outcome) return { outcome, summary: null };
3573
+ const token = await this.hubActionStore!.token(ownerDid, actionId);
3574
+ return { outcome, summary: bankReceiptSummary(outcome.receipt, token) };
3575
+ }
3576
+
3388
3577
  private async statusHubAction(
3389
3578
  ownerDid: string,
3390
3579
  actionId: string,
@@ -3475,15 +3664,24 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3475
3664
  input.reference.id,
3476
3665
  );
3477
3666
  if (!held || !this.hubActionClient) throw new Error('Unknown Hub action');
3667
+ if (canonicalJson(input.reference) !== canonicalJson(held.reference))
3668
+ throw new Error('Hub outcome does not match the prepared action');
3669
+ // A replay of an outcome already confirmed and stored (a Hub retry,
3670
+ // a callback delivered twice) is acknowledged without a Hub read.
3671
+ const stored = await this.hubActionStore!.outcomeAt(
3672
+ input.ownerDid,
3673
+ input.reference.id,
3674
+ input.sequence,
3675
+ );
3676
+ if (stored && sameHubOutcome(stored, input)) return;
3478
3677
  const remote = await this.hubActionClient.status(input.reference.id);
3479
3678
  if (
3480
3679
  !remote.outcome ||
3481
3680
  remote.ownerDid !== input.ownerDid ||
3482
3681
  canonicalJson(remote.reference) !== canonicalJson(held.reference) ||
3483
- canonicalJson(input.reference) !== canonicalJson(held.reference) ||
3484
3682
  remote.outcome.sequence < input.sequence ||
3485
3683
  (remote.outcome.sequence === input.sequence &&
3486
- canonicalJson(remote.outcome) !== canonicalJson(input))
3684
+ !sameHubOutcome(remote.outcome, input))
3487
3685
  )
3488
3686
  throw new Error(
3489
3687
  'Hub outcome was not confirmed by authenticated status',
@@ -3732,13 +3930,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3732
3930
  input.evidenceRefs,
3733
3931
  );
3734
3932
  if (prior) return prior;
3735
- // The caps before the authority read it would waste (create checks
3736
- // them again, atomically).
3737
- await topics.assertCapacity(
3738
- this.userDid,
3739
- input.origin.sessionId,
3740
- input.origin.runId,
3741
- );
3742
3933
  const rpc = {
3743
3934
  devnet: this.env.ACTION_PROPOSALS_RPC_DEVNET,
3744
3935
  testnet: this.env.ACTION_PROPOSALS_RPC_TESTNET,
@@ -6691,54 +6882,34 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
6691
6882
  ambientLlm: LlmAdapter;
6692
6883
  platform: OpenRouterLlmAdapter;
6693
6884
  signal: AbortSignal;
6885
+ /** Attachments of the turn now running (not necessarily persisted yet). */
6886
+ turnMetas: readonly AttachmentMeta[];
6694
6887
  }): Promise<AttachmentViewSurface> {
6695
6888
  const saver = this.saver!;
6696
6889
  const offloaded = await saver.hasThreadMessageMatching(input.sessionId, {
6697
6890
  messageType: 'human',
6698
6891
  contentContains: ATTACHMENT_PLACEHOLDER_PREFIX,
6699
6892
  });
6700
- const findMeta = async (ref: string) => {
6701
- // The placeholder quotes the ref, so a cheap pre-filter usually
6702
- // finds the message; a still-inline attachment needs the full scan.
6703
- const candidates = await saver.listThreadMessagesMatching(
6704
- input.sessionId,
6705
- { messageType: 'human', contentContains: ref },
6706
- );
6707
- const scan =
6708
- candidates.length > 0
6709
- ? candidates
6710
- : await saver.listThreadMessages(input.sessionId);
6711
- return scan
6712
- .filter((m) => m.type === 'human')
6713
- .flatMap((m) => attachmentMetas(m))
6714
- .find((meta) => attachmentRef(meta) === ref);
6715
- };
6716
- return {
6893
+ const findMeta = (ref: string) =>
6894
+ findPersistedAttachmentMeta(saver, input.sessionId, ref);
6895
+ const viewDeps = (): ViewAttachmentDeps => ({
6896
+ source: this.matrixMediaSource(),
6897
+ extraction: this.extractionProvider(input.ambientLlm, input.platform),
6898
+ caps: getModelCapabilities(input.model),
6899
+ roomId: input.roomId,
6900
+ signal: input.signal,
6901
+ logger: console,
6902
+ ...(this.attachmentTextCache
6903
+ ? { cache: this.attachmentTextCache.forSession(input.sessionId) }
6904
+ : {}),
6905
+ ingestCache: this.ingestTextLru.forSession(input.sessionId),
6906
+ });
6907
+ return createAttachmentViewSurface({
6717
6908
  offloaded,
6718
- view: async (ref) => {
6719
- const meta = await findMeta(ref);
6720
- if (!meta) {
6721
- throw new Error(
6722
- `no attachment with ref "${ref}" in this conversation`,
6723
- );
6724
- }
6725
- const view = await viewAttachment(meta, {
6726
- source: this.matrixMediaSource(),
6727
- extraction: this.extractionProvider(
6728
- input.ambientLlm,
6729
- input.platform,
6730
- ),
6731
- caps: getModelCapabilities(input.model),
6732
- roomId: input.roomId,
6733
- signal: input.signal,
6734
- logger: console,
6735
- ...(this.attachmentTextCache
6736
- ? { cache: this.attachmentTextCache.forSession(input.sessionId) }
6737
- : {}),
6738
- });
6739
- return { meta, view };
6740
- },
6741
- };
6909
+ turnMetas: input.turnMetas,
6910
+ findPersisted: findMeta,
6911
+ deps: viewDeps,
6912
+ });
6742
6913
  }
6743
6914
 
6744
6915
  /**
@@ -7114,6 +7285,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7114
7285
  ambientLlm: baseAmbient.llm,
7115
7286
  platform: core.llm,
7116
7287
  signal: abortController.signal,
7288
+ turnMetas: prepared?.metas ?? [],
7117
7289
  });
7118
7290
 
7119
7291
  // Request metadata (editor room, space, session run, entity) → state,
@@ -7511,36 +7683,30 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7511
7683
  ? undefined
7512
7684
  : renderTurnTimeNote(turnAt, req.identity.timezone);
7513
7685
  const content = timeNote ? withTurnTimeNote(spoken, timeNote) : spoken;
7686
+ const human = new HumanMessage({
7687
+ content,
7688
+ additional_kwargs: {
7689
+ timestamp: turnAt.toISOString(),
7690
+ ...(timeNote ? { [TURN_TIME_NOTE_KWARG]: timeNote } : {}),
7691
+ oracleName: core.identity.name,
7692
+ msgFromMatrixRoom: req.client === 'matrix',
7693
+ ...speakerKwargs,
7694
+ ...(req.channel
7695
+ ? { 'org.ixo.qi.origin': channelOrigin(req.channel) }
7696
+ : {}),
7697
+ ...attachmentKwargs,
7698
+ },
7699
+ });
7514
7700
  const stateInput = {
7515
- messages: [
7516
- new HumanMessage({
7517
- content,
7518
- additional_kwargs: {
7519
- timestamp: turnAt.toISOString(),
7520
- ...(timeNote ? { [TURN_TIME_NOTE_KWARG]: timeNote } : {}),
7521
- oracleName: core.identity.name,
7522
- msgFromMatrixRoom: req.client === 'matrix',
7523
- ...speakerKwargs,
7524
- ...(req.channel
7525
- ? { 'org.ixo.qi.origin': channelOrigin(req.channel) }
7526
- : {}),
7527
- ...attachmentKwargs,
7528
- },
7529
- }),
7530
- // Extraction-lane text rides as hidden context messages (the
7531
- // transcript hides AI messages that carry an `attachment`), as on Node.
7532
- ...(prepared?.extracted ?? []).map(
7533
- (item) =>
7534
- new AIMessage({
7535
- content: item.text,
7536
- additional_kwargs: {
7537
- msgFromMatrixRoom: req.client === 'matrix',
7538
- timestamp: new Date().toISOString(),
7539
- attachment: item.meta,
7540
- },
7541
- }),
7542
- ),
7543
- ],
7701
+ // Hidden attachment context (the transcript hides AI messages that
7702
+ // carry an `attachment`): native-lane notes precede the person's
7703
+ // message, extraction-lane text follows it, as on Node.
7704
+ messages: assembleTurnMessages({
7705
+ human,
7706
+ extracted: prepared?.extracted ?? [],
7707
+ nativeNotes: prepared?.nativeNotes ?? [],
7708
+ matrixRoom: req.client === 'matrix',
7709
+ }),
7544
7710
  config: { did: req.identity.userDid },
7545
7711
  client: req.client,
7546
7712
  // First contact's gathered context is checkpointed with the turn,