@ixo/oracle-runtime-workers 0.30.0 → 0.32.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.
Files changed (53) hide show
  1. package/package.json +7 -5
  2. package/src/attachments/channel-links.ts +135 -0
  3. package/src/attachments/channel.ts +203 -0
  4. package/src/attachments/download.ts +23 -6
  5. package/src/attachments/index.ts +23 -0
  6. package/src/attachments/pipeline.ts +120 -42
  7. package/src/attachments/retention.ts +11 -3
  8. package/src/attachments/sanitize.ts +20 -8
  9. package/src/attachments/types.ts +7 -0
  10. package/src/attachments/view.ts +24 -7
  11. package/src/channels/contract.ts +69 -0
  12. package/src/channels/turns.ts +23 -1
  13. package/src/core/main-agent-types.ts +2 -0
  14. package/src/core/middlewares/tool-repetition-guard.ts +6 -3
  15. package/src/core/middlewares/turn-boundary.ts +6 -1
  16. package/src/core/runtime-context.ts +3 -0
  17. package/src/core/tool-not-run.ts +30 -0
  18. package/src/core/wrap-plugin-tool.ts +9 -1
  19. package/src/delivery/plan.ts +3 -1
  20. package/src/do/contracts.ts +3 -0
  21. package/src/do/turn-attachments.ts +103 -0
  22. package/src/do/user-oracle-do.ts +220 -57
  23. package/src/hub-actions/chain-tx-refusals.ts +217 -0
  24. package/src/hub-actions/client.ts +170 -26
  25. package/src/hub-actions/hub-text.ts +82 -0
  26. package/src/hub-actions/request.ts +39 -29
  27. package/src/hub-actions/store.ts +5 -1
  28. package/src/hub-actions/transfer.ts +99 -32
  29. package/src/index.ts +1 -0
  30. package/src/plugin-api/index.ts +1 -0
  31. package/src/plugin-api/types.ts +102 -20
  32. package/src/plugins/attachments/view-attachment-tool.ts +7 -3
  33. package/src/plugins/hub-actions/chain-tx-catalog.ts +1239 -0
  34. package/src/plugins/hub-actions/hub-actions.plugin.ts +490 -232
  35. package/src/plugins/ixo-chain/blocksync.ts +6 -2
  36. package/src/plugins/ixo-chain/http.ts +16 -8
  37. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +1 -1
  38. package/src/plugins/ixo-chain/kinds/chain/index.ts +2 -0
  39. package/src/plugins/ixo-chain/kinds/chain/validators.ts +151 -0
  40. package/src/plugins/ixo-chain/rest.ts +7 -1
  41. package/src/plugins/yoma/catalog.ts +780 -0
  42. package/src/plugins/yoma/clef.ts +98 -0
  43. package/src/plugins/yoma/drafts.ts +339 -0
  44. package/src/plugins/yoma/engine.ts +978 -0
  45. package/src/plugins/yoma/index.ts +13 -0
  46. package/src/plugins/yoma/judge.ts +358 -0
  47. package/src/plugins/yoma/permission.ts +62 -0
  48. package/src/plugins/yoma/reads.ts +195 -0
  49. package/src/plugins/yoma/submission.ts +531 -0
  50. package/src/plugins/yoma/text.ts +16 -0
  51. package/src/plugins/yoma/work.ts +190 -0
  52. package/src/plugins/yoma/yoma.plugin.ts +2171 -0
  53. package/src/sqlite/serialization.ts +2 -0
@@ -46,11 +46,15 @@ import type {
46
46
  ActionProposalSubject,
47
47
  } from '@ixo/ixo-transaction/action-proposals';
48
48
  import { ChannelTurns } from '../channels/turns';
49
- import { HubActionClient } from '../hub-actions/client';
49
+ import {
50
+ HubActionClient,
51
+ type HubOperationStatus,
52
+ type HubReviewStatus,
53
+ } from '../hub-actions/client';
50
54
  import { chainTxOperation, prepareHubAction } from '../hub-actions/request';
51
55
  import {
52
56
  bankReceiptSummary,
53
- bankStepsInBaseUnits,
57
+ chainTxAmountsInBaseUnits,
54
58
  } from '../hub-actions/transfer';
55
59
  import {
56
60
  HUB_CHECK_AFTER_EXPIRY_MS,
@@ -126,6 +130,7 @@ import {
126
130
  offloadInlinePayloads,
127
131
  parseAttachmentInputs,
128
132
  prepareAttachments,
133
+ ChannelAttachmentLinkStore,
129
134
  type AttachmentViewSurface,
130
135
  type MatrixMediaSource,
131
136
  type ViewAttachmentDeps,
@@ -135,6 +140,12 @@ import {
135
140
  readBytesCapped,
136
141
  AttachmentTextCacheStore,
137
142
  } from '../attachments';
143
+ import {
144
+ channelTurnLinks,
145
+ prepareTurnAttachments,
146
+ turnAttachmentInputs,
147
+ turnChannelMediaSource,
148
+ } from './turn-attachments';
138
149
  import type { RuntimeCore } from '../core';
139
150
  import {
140
151
  createLlmAdapter,
@@ -430,6 +441,7 @@ import {
430
441
  } from './ucan-service';
431
442
  import type {
432
443
  ChainTxMessageInput,
444
+ HubActionStatusRead,
433
445
  UcanDelegation,
434
446
  UserContextData,
435
447
  } from '../plugin-api/types';
@@ -564,6 +576,7 @@ function hasUserContext(value: unknown): boolean {
564
576
  function admissionApplies(req: TurnRequest): boolean {
565
577
  return (
566
578
  !req.attachments?.length &&
579
+ !req.channel?.attachments?.length &&
567
580
  !req.taskRunId &&
568
581
  !req.sessionId.startsWith(TASK_SESSION_PREFIX) &&
569
582
  req.roomKind !== 'group'
@@ -625,6 +638,14 @@ const ACTION_TICK_BUDGET_MS = 15_000;
625
638
  const ACTION_PRUNE_INTERVAL_MS = 60 * 60_000;
626
639
  /** How long the Hub's capabilities answer is reused per object. */
627
640
  const HUB_CAPABILITIES_CACHE_MS = 60_000;
641
+ /**
642
+ * A capabilities read on a turn's critical path (the chain transaction
643
+ * tool's types): a short timeout, and after a failed read none for this
644
+ * long, so a slow or failing Hub costs a turn at most this timeout once per
645
+ * window instead of the full Hub timeout on every turn.
646
+ */
647
+ const HUB_TURN_CAPABILITIES_TIMEOUT_MS = 3_000;
648
+ const HUB_CAPABILITIES_FAILURE_CACHE_MS = 30_000;
628
649
  /** A Hub network name as the chain-id config key it is configured under. */
629
650
  /**
630
651
  * Whether two Hub outcomes are the same event. Receipts are compared in the
@@ -1041,11 +1062,17 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1041
1062
  private actionProposals: ActionProposalStore | null = null;
1042
1063
  private hubActionStore: HubActionStore | null = null;
1043
1064
  private hubActionClient: HubActionClient | null = null;
1044
- /** The Hub's capabilities for this oracle account, reused for `HUB_CAPABILITIES_CACHE_MS`. */
1065
+ /** The Hub's capabilities for this oracle account, reused for `HUB_CAPABILITIES_CACHE_MS` (and on a turn, while a read fails). */
1045
1066
  private hubCapabilities: {
1046
1067
  value: ActionCapabilitiesV1;
1047
1068
  at: number;
1048
1069
  } | null = null;
1070
+ /** The last failed capabilities read: a turn does not read again within `HUB_CAPABILITIES_FAILURE_CACHE_MS`. */
1071
+ private hubCapabilitiesFailure: { error: unknown; at: number } | null =
1072
+ null;
1073
+ /** A turn's capabilities read in flight, shared by concurrent turns. */
1074
+ private hubTurnCapabilitiesRead: Promise<ActionCapabilitiesV1> | null =
1075
+ null;
1049
1076
  /** The last `hub_action_options` answer for this owner (Gmail connection checks). */
1050
1077
  private lastHubOptions: {
1051
1078
  value: HubActionOptionsV2 | HubActionOptionsV3;
@@ -1058,6 +1085,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1058
1085
  private lastActionPruneAt: number | null = null;
1059
1086
  /** Text `view_attachment` extracted, per session (`attachments/view-cache.ts`). */
1060
1087
  private attachmentTextCache: AttachmentTextCacheStore | null = null;
1088
+ /** The links behind the `channel:` references of each session's files. */
1089
+ private channelAttachmentLinks: ChannelAttachmentLinkStore | null = null;
1061
1090
  /** Large texts of document ingestion, in memory (a few, minutes). */
1062
1091
  private readonly ingestTextLru = new IngestTextLru();
1063
1092
  /**
@@ -2019,6 +2048,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2019
2048
  });
2020
2049
  await this.resultStore.setup();
2021
2050
  this.attachmentTextCache = new AttachmentTextCacheStore(liveDb, console);
2051
+ this.channelAttachmentLinks = new ChannelAttachmentLinkStore(
2052
+ liveDb,
2053
+ console,
2054
+ );
2022
2055
  // Constructing it touches no SQL. Only an observe turn records (and so
2023
2056
  // creates `domain_context_runs`); a session delete never creates it but
2024
2057
  // still removes rows an earlier observe deployment left behind.
@@ -2258,7 +2291,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2258
2291
  ...(this.hubActionStore && this.hubActionClient
2259
2292
  ? {
2260
2293
  hubActions: {
2261
- capabilities: () => this.cachedHubCapabilities(),
2294
+ capabilities: (opts) =>
2295
+ opts?.turn
2296
+ ? this.turnHubCapabilities()
2297
+ : this.cachedHubCapabilities(),
2262
2298
  options: (input) => this.hubActionOptions(input),
2263
2299
  request: (input: {
2264
2300
  ownerDid: string;
@@ -2273,8 +2309,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2273
2309
  }) => this.requestHubAction(input),
2274
2310
  chainTransaction: (input) =>
2275
2311
  this.chainTransactionHubAction(input),
2276
- status: (ownerDid: string, actionId: string) =>
2277
- this.hubActionStatusForTool(ownerDid, actionId),
2312
+ status: (
2313
+ ownerDid: string,
2314
+ actionId: string,
2315
+ opts?: { timeoutMs?: number },
2316
+ ) => this.hubActionStatusForTool(ownerDid, actionId, opts),
2278
2317
  },
2279
2318
  }
2280
2319
  : {}),
@@ -2363,9 +2402,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2363
2402
  this.hubActionStore = null;
2364
2403
  this.hubActionClient = null;
2365
2404
  this.hubCapabilities = null;
2405
+ this.hubCapabilitiesFailure = null;
2406
+ this.hubTurnCapabilitiesRead = null;
2366
2407
  this.lastHubOptions = null;
2367
2408
  this.topicProposals = null;
2368
2409
  this.attachmentTextCache = null;
2410
+ this.channelAttachmentLinks = null;
2369
2411
  this.domainContextStore = null;
2370
2412
  // Its store is bound to the database just dropped: a caller that sees
2371
2413
  // no scheduler boots again instead of reading a closed connection.
@@ -3329,7 +3371,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3329
3371
  // The gateway parses with @ixo/action-protocol 0.4.1, whose bank
3330
3372
  // receipt is exact: the 0.5.0 `denom`/`amountBaseUnits` fields are
3331
3373
  // dropped here (the owner-facing amount line comes from
3332
- // hub_action_status instead), so the emitted shape stays 0.4.1.
3374
+ // hub_action_status instead), so the emitted shape stays 0.4.1. Every
3375
+ // other receipt goes as Auth Hub signed it, a failure's `detail`
3376
+ // included: the gateway confirms an outcome by comparing the whole
3377
+ // receipt with Auth Hub's copy. Only what the model sees drops it.
3333
3378
  const hubActionOutcomes = (
3334
3379
  await this.hubOutcomesForRun(ownerDid, result.sessionId, result.runId)
3335
3380
  ).map((outcome) => ({ ...outcome, receipt: v1Receipt(outcome.receipt) }));
@@ -3498,16 +3543,57 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3498
3543
  }
3499
3544
 
3500
3545
  /** The Hub's capabilities, cached per object for `HUB_CAPABILITIES_CACHE_MS`. */
3501
- private async cachedHubCapabilities(): Promise<ActionCapabilitiesV1> {
3546
+ private async cachedHubCapabilities(
3547
+ timeoutMs?: number,
3548
+ ): Promise<ActionCapabilitiesV1> {
3502
3549
  const client = this.hubActionClient;
3503
3550
  if (!client) throw new Error('Hub actions are unavailable');
3504
3551
  const now = Date.now();
3505
3552
  const cached = this.hubCapabilities ?? null;
3506
3553
  if (cached && now - cached.at < HUB_CAPABILITIES_CACHE_MS)
3507
3554
  return cached.value;
3508
- const value = await client.capabilities();
3509
- this.hubCapabilities = { value, at: now };
3510
- return value;
3555
+ try {
3556
+ const value = await client.capabilities(timeoutMs);
3557
+ this.hubCapabilities = { value, at: Date.now() };
3558
+ this.hubCapabilitiesFailure = null;
3559
+ return value;
3560
+ } catch (error) {
3561
+ this.hubCapabilitiesFailure = { error, at: Date.now() };
3562
+ throw error;
3563
+ }
3564
+ }
3565
+
3566
+ /**
3567
+ * The capabilities for a turn's tool list: the cached ones while fresh;
3568
+ * else one read with `HUB_TURN_CAPABILITIES_TIMEOUT_MS`, shared by
3569
+ * concurrent turns and not repeated within
3570
+ * `HUB_CAPABILITIES_FAILURE_CACHE_MS` of a failed read. While reads
3571
+ * fail, the last capabilities read are answered (the Hub enforces its
3572
+ * own on every prepare); with none, the failure.
3573
+ */
3574
+ private async turnHubCapabilities(): Promise<ActionCapabilitiesV1> {
3575
+ const cached = this.hubCapabilities;
3576
+ if (cached && Date.now() - cached.at < HUB_CAPABILITIES_CACHE_MS)
3577
+ return cached.value;
3578
+ const failure = this.hubCapabilitiesFailure;
3579
+ if (
3580
+ failure &&
3581
+ Date.now() - failure.at < HUB_CAPABILITIES_FAILURE_CACHE_MS
3582
+ ) {
3583
+ if (cached) return cached.value;
3584
+ throw failure.error;
3585
+ }
3586
+ this.hubTurnCapabilitiesRead ??= this.cachedHubCapabilities(
3587
+ HUB_TURN_CAPABILITIES_TIMEOUT_MS,
3588
+ ).finally(() => {
3589
+ this.hubTurnCapabilitiesRead = null;
3590
+ });
3591
+ try {
3592
+ return await this.hubTurnCapabilitiesRead;
3593
+ } catch (error) {
3594
+ if (cached) return cached.value;
3595
+ throw error;
3596
+ }
3511
3597
  }
3512
3598
 
3513
3599
  /**
@@ -3560,13 +3646,14 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3560
3646
  /**
3561
3647
  * One chain transaction of the model's messages. The network is the
3562
3648
  * account's capability, the chain id this Worker's own for that
3563
- * network; the signer and the owner's identity in claims messages come
3649
+ * network; the signer and the owner's identity in the messages come
3564
3650
  * from the owner's DID (`chainTxOperation`). The model names none of
3565
- * them. Bank steps name a listed token and a display amount: with at
3566
- * least one, the owner's options are read once, fresh (the balances
3567
- * must be current), and each step is converted to base units and
3568
- * checked against the listed balance (`bankStepsInBaseUnits`). No fee
3569
- * is sent: the Hub simulates gas and sets it.
3651
+ * them. Bank steps name a listed token and a display amount, stake
3652
+ * steps an amount of IXO: with at least one of either, the owner's
3653
+ * options are read once, fresh (the balances must be current), and each
3654
+ * amount is converted to base units and checked against the listed
3655
+ * balance (`chainTxAmountsInBaseUnits`). No fee is sent: the Hub
3656
+ * simulates gas and sets it.
3570
3657
  */
3571
3658
  private async chainTransactionHubAction(input: {
3572
3659
  ownerDid: string;
@@ -3598,7 +3685,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3598
3685
  HUB_NETWORK_CHAIN[capabilities.network]
3599
3686
  ];
3600
3687
  const tokens = input.messages.some(
3601
- (message) => message.type === 'cosmos.bank.send',
3688
+ (message) =>
3689
+ message.type === 'cosmos.bank.send' ||
3690
+ message.type === 'cosmos.staking.delegate',
3602
3691
  )
3603
3692
  ? ((await this.hubActionOptions(input, true)).transfer?.tokens ?? null)
3604
3693
  : null;
@@ -3607,30 +3696,64 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3607
3696
  chainId,
3608
3697
  network: capabilities.network,
3609
3698
  memo: input.memo,
3610
- messages: bankStepsInBaseUnits(input.messages, tokens),
3699
+ messages: chainTxAmountsInBaseUnits(input.messages, tokens),
3611
3700
  });
3612
3701
  return this.requestHubAction({ ...input, operation }, true);
3613
3702
  }
3614
3703
 
3615
3704
  /**
3616
- * `hub_action_status`: the outcome plus, for a bank send, the
3617
- * owner-facing line (amount from the token stored at preparation).
3705
+ * `ctx.hubActions.status` (and `hub_action_status`): Auth Hub's review
3706
+ * and operation status, the outcome and, for a bank send, the
3707
+ * owner-facing line (amount from the token stored at preparation). An
3708
+ * action this object never prepared is `held: false`, without a Hub
3709
+ * read.
3618
3710
  */
3619
3711
  private async hubActionStatusForTool(
3620
3712
  ownerDid: string,
3621
3713
  actionId: string,
3622
- ): Promise<{ outcome: ActionOutcomeV1 | null; summary: string | null }> {
3623
- const outcome = await this.statusHubAction(ownerDid, actionId);
3624
- if (!outcome) return { outcome, summary: null };
3625
- const token = await this.hubActionStore!.token(ownerDid, actionId);
3626
- return { outcome, summary: bankReceiptSummary(outcome.receipt, token) };
3714
+ opts: { timeoutMs?: number } = {},
3715
+ ): Promise<HubActionStatusRead> {
3716
+ const read = await this.statusHubAction(
3717
+ ownerDid,
3718
+ actionId,
3719
+ opts.timeoutMs,
3720
+ );
3721
+ if (!read)
3722
+ return {
3723
+ held: false,
3724
+ reviewStatus: null,
3725
+ operationStatus: null,
3726
+ outcome: null,
3727
+ summary: null,
3728
+ };
3729
+ const token = read.outcome
3730
+ ? await this.hubActionStore!.token(ownerDid, actionId)
3731
+ : null;
3732
+ return {
3733
+ held: true,
3734
+ reviewStatus: read.reviewStatus,
3735
+ operationStatus: read.operationStatus,
3736
+ outcome: read.outcome,
3737
+ summary: read.outcome
3738
+ ? bankReceiptSummary(read.outcome.receipt, token)
3739
+ : null,
3740
+ };
3627
3741
  }
3628
3742
 
3743
+ /**
3744
+ * One authenticated status read of a held action (null: not held), its
3745
+ * outcome recorded when new. An outcome for another revision than the
3746
+ * one Auth Hub reports is not taken (`outcome: null`).
3747
+ */
3629
3748
  private async statusHubAction(
3630
3749
  ownerDid: string,
3631
3750
  actionId: string,
3632
3751
  timeoutMs?: number,
3633
- ): Promise<ActionOutcomeV1 | null> {
3752
+ ): Promise<{
3753
+ reviewStatus: HubReviewStatus;
3754
+ operationStatus: HubOperationStatus;
3755
+ outcome: ActionOutcomeV1 | null;
3756
+ } | null> {
3634
3757
  const held = await this.hubActionStore?.get(ownerDid, actionId);
3635
3758
  if (!held || !this.hubActionClient) return null;
3636
3759
  const status = await this.hubActionClient.status(actionId, timeoutMs);
@@ -3639,16 +3762,20 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3639
3762
  canonicalJson(status.reference) !== canonicalJson(held.reference)
3640
3763
  )
3641
3764
  throw new Error('Hub status does not match the prepared action');
3765
+ const read = {
3766
+ reviewStatus: status.reviewStatus,
3767
+ operationStatus: status.operationStatus,
3768
+ };
3642
3769
  if (status.outcome) {
3643
3770
  if (
3644
3771
  status.revision !== status.outcome.authorizedRevision ||
3645
3772
  status.digest !== status.outcome.authorizedDigest
3646
3773
  )
3647
- return null;
3774
+ return { ...read, outcome: null };
3648
3775
  if (await this.hubActionStore!.recordOutcome(status.outcome))
3649
3776
  this.markDirty();
3650
3777
  }
3651
- return status.outcome;
3778
+ return { ...read, outcome: status.outcome };
3652
3779
  }
3653
3780
 
3654
3781
  /**
@@ -3687,11 +3814,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3687
3814
  // refreshed writes it, at most one per poll.
3688
3815
  this.markDirty();
3689
3816
  try {
3690
- const fresh = await this.statusHubAction(
3691
- ownerDid,
3692
- row.reference.id,
3693
- HUB_POLL_TIMEOUT_MS,
3694
- );
3817
+ const fresh = (
3818
+ await this.statusHubAction(
3819
+ ownerDid,
3820
+ row.reference.id,
3821
+ HUB_POLL_TIMEOUT_MS,
3822
+ )
3823
+ )?.outcome;
3695
3824
  if (fresh && (!latest || fresh.sequence > latest.sequence))
3696
3825
  latest = fresh;
3697
3826
  } catch (error) {
@@ -3848,6 +3977,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3848
3977
  `[user-do] could not delete the cached attachment text of ${sessionId}: ${err instanceof Error ? err.message : String(err)}`,
3849
3978
  );
3850
3979
  });
3980
+ await this.channelAttachmentLinks
3981
+ ?.forgetSession(sessionId)
3982
+ .catch((err: unknown) => {
3983
+ console.warn(
3984
+ `[user-do] could not delete the channel file links of ${sessionId}; they expire with the links: ${err instanceof Error ? err.message : String(err)}`,
3985
+ );
3986
+ });
3851
3987
  await this.resultStore
3852
3988
  ?.deleteForSession(sessionId)
3853
3989
  .catch((err: unknown) => {
@@ -7038,6 +7174,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7038
7174
  signal: AbortSignal;
7039
7175
  /** Attachments of the turn now running (not necessarily persisted yet). */
7040
7176
  turnMetas: readonly AttachmentMeta[];
7177
+ /** The links of this turn's channel files (earlier ones come from the store). */
7178
+ channelLinks: ReturnType<typeof channelTurnLinks>;
7041
7179
  }): Promise<AttachmentViewSurface> {
7042
7180
  const saver = this.saver!;
7043
7181
  const offloaded = await saver.hasThreadMessageMatching(input.sessionId, {
@@ -7048,6 +7186,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7048
7186
  findPersistedAttachmentMeta(saver, input.sessionId, ref);
7049
7187
  const viewDeps = (): ViewAttachmentDeps => ({
7050
7188
  source: this.matrixMediaSource(),
7189
+ channel: turnChannelMediaSource({
7190
+ sessionId: input.sessionId,
7191
+ turnLinks: input.channelLinks,
7192
+ store: this.channelAttachmentLinks,
7193
+ channelServiceDid: this.env.CHANNEL_SERVICE_DID,
7194
+ }),
7051
7195
  extraction: this.extractionProvider(input.ambientLlm, input.platform),
7052
7196
  caps: getModelCapabilities(input.model),
7053
7197
  roomId: input.roomId,
@@ -7390,32 +7534,47 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7390
7534
  // native capabilities, send images/files inline when it reads them,
7391
7535
  // otherwise the `vision` role turns them into text on the platform key;
7392
7536
  // originals are archived to the user's sandbox when one is reachable.
7393
- const attachmentInputs = parseAttachmentInputs(
7394
- body.attachments ?? req.attachments,
7395
- );
7537
+ // A channel turn's files are read by their `channel:` reference; their
7538
+ // links stay on the server (the run request and the session's link
7539
+ // store), out of every message and note.
7540
+ const attachmentInputs = turnAttachmentInputs(req, body.attachments);
7396
7541
  const attachmentModel =
7397
7542
  effectiveModel && isAllowedModel(effectiveModel)
7398
7543
  ? effectiveModel
7399
7544
  : getDefaultModelId(core.validatedEnv);
7400
- const prepared =
7401
- attachmentInputs.length > 0
7402
- ? await prepareAttachments(
7403
- {
7404
- text: body.message,
7405
- attachments: attachmentInputs,
7406
- roomId: sessionRoomId,
7407
- model: attachmentModel,
7408
- caps: getModelCapabilities(attachmentModel),
7409
- },
7410
- {
7411
- source: this.matrixMediaSource(),
7412
- extraction: this.extractionProvider(baseAmbient.llm, core.llm),
7413
- sandbox: await this.sandboxArchiveConfig(req.identity.userDid),
7414
- signal: abortController.signal,
7415
- logger: console,
7416
- },
7417
- )
7418
- : null;
7545
+ const turnLinks = channelTurnLinks(req);
7546
+ const channelMedia = turnLinks.length
7547
+ ? turnChannelMediaSource({
7548
+ sessionId: req.sessionId,
7549
+ turnLinks,
7550
+ store: this.channelAttachmentLinks,
7551
+ channelServiceDid: this.env.CHANNEL_SERVICE_DID,
7552
+ })
7553
+ : undefined;
7554
+ if (turnLinks.length && !run.resumed)
7555
+ await this.channelAttachmentLinks?.put(req.sessionId, turnLinks);
7556
+ const { prepared, metas: turnMetas } = await prepareTurnAttachments({
7557
+ inputs: attachmentInputs,
7558
+ resumed: run.resumed,
7559
+ prepare: async () =>
7560
+ prepareAttachments(
7561
+ {
7562
+ text: body.message,
7563
+ attachments: attachmentInputs,
7564
+ roomId: sessionRoomId,
7565
+ model: attachmentModel,
7566
+ caps: getModelCapabilities(attachmentModel),
7567
+ },
7568
+ {
7569
+ source: this.matrixMediaSource(),
7570
+ ...(channelMedia ? { channel: channelMedia } : {}),
7571
+ extraction: this.extractionProvider(baseAmbient.llm, core.llm),
7572
+ sandbox: await this.sandboxArchiveConfig(req.identity.userDid),
7573
+ signal: abortController.signal,
7574
+ logger: console,
7575
+ },
7576
+ ),
7577
+ });
7419
7578
  if (prepared) {
7420
7579
  console.log(
7421
7580
  `[user-do] attachments: ${attachmentInputs.length} attached, ${prepared.extracted.length} extracted, ${Array.isArray(prepared.content) ? prepared.content.length - 1 : 0} native`,
@@ -7439,7 +7598,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7439
7598
  ambientLlm: baseAmbient.llm,
7440
7599
  platform: core.llm,
7441
7600
  signal: abortController.signal,
7442
- turnMetas: prepared?.metas ?? [],
7601
+ turnMetas,
7602
+ channelLinks: turnLinks,
7443
7603
  });
7444
7604
 
7445
7605
  // Request metadata (editor room, space, session run, entity) → state,
@@ -7773,6 +7933,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7773
7933
  provider: req.channel.provider,
7774
7934
  bindingId: req.channel.bindingId,
7775
7935
  bindingRevision: req.channel.bindingRevision,
7936
+ ...(req.channel.attachments?.length
7937
+ ? { attachments: req.channel.attachments }
7938
+ : {}),
7776
7939
  },
7777
7940
  }
7778
7941
  : {}),