@ixo/oracle-runtime-workers 0.25.0 → 0.27.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/package.json +3 -2
- package/src/attachments/extract.ts +108 -14
- package/src/attachments/index.ts +15 -0
- package/src/attachments/pdf-text.ts +99 -0
- package/src/attachments/pipeline.ts +47 -4
- package/src/attachments/sanitize.ts +14 -0
- package/src/attachments/view-cache.ts +95 -0
- package/src/attachments/view.ts +308 -3
- package/src/channels/auth.ts +2 -1
- package/src/channels/contract.ts +104 -0
- package/src/channels/replies.ts +58 -0
- package/src/channels/routing.ts +222 -0
- package/src/channels/turns.ts +1252 -16
- package/src/core/env.ts +17 -0
- package/src/do/contracts.ts +19 -0
- package/src/do/room-mirror.ts +8 -1
- package/src/do/run-coordinator.ts +27 -0
- package/src/do/transcript.ts +14 -11
- package/src/do/turn-messages.ts +54 -0
- package/src/do/user-oracle-do.ts +321 -135
- package/src/memory/session-history-indexer.ts +442 -103
- package/src/memory/user-context-fetcher.ts +13 -0
- package/src/memory/user-context-invalidation.ts +8 -2
- package/src/plugins/action-proposals/evidence.ts +7 -8
- package/src/plugins/memory/document-chunker.ts +313 -0
- package/src/plugins/memory/document-source.ts +229 -0
- package/src/plugins/memory/memory-engine-client.ts +352 -0
- package/src/plugins/memory/memory.plugin.ts +25 -2
- package/src/plugins/memory/oracle-document-tools.ts +677 -0
- package/src/plugins/tasks/middleware.ts +4 -9
- package/src/plugins/vfs/index.ts +11 -2
- package/src/plugins/vfs/vfs-content.ts +86 -22
- package/src/plugins/vfs/vfs.plugin.ts +28 -19
- package/src/shell/app.ts +70 -0
- package/src/sqlite/serialization.ts +20 -0
- package/src/sqlite/sessions-store.ts +43 -0
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
channelReplyNote,
|
|
3
|
+
CHANNEL_REPLY_NOTE_KWARG,
|
|
4
|
+
} from '../channels/replies';
|
|
5
|
+
import { channelRoutingConfig } from '../channels/routing';
|
|
1
6
|
import { digestTopicProposalEffect } from '../action-proposals/topic-store';
|
|
2
7
|
import {
|
|
3
8
|
chainIdsFromConfig,
|
|
@@ -22,7 +27,11 @@ import {
|
|
|
22
27
|
type BeginTopicStep,
|
|
23
28
|
type TopicReceiptReferences,
|
|
24
29
|
} from '@ixo/ixo-transaction/topic-proposals';
|
|
25
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
ChannelControlInput,
|
|
32
|
+
ChannelControlOutcome,
|
|
33
|
+
ChannelTurnResponse,
|
|
34
|
+
} from '../channels/contract';
|
|
26
35
|
import { refreshActionProposal } from '../action-proposals/reconcile';
|
|
27
36
|
import { assertActionProposalPolicy } from '../action-proposals/policy';
|
|
28
37
|
import { TopicReadUnavailable } from '../action-proposals/topic-errors';
|
|
@@ -111,16 +120,17 @@ import {
|
|
|
111
120
|
applyAttachmentRetention,
|
|
112
121
|
ATTACHMENT_PAYLOAD_TURNS,
|
|
113
122
|
ATTACHMENT_PLACEHOLDER_PREFIX,
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
createAttachmentViewSurface,
|
|
124
|
+
findPersistedAttachmentMeta,
|
|
125
|
+
IngestTextLru,
|
|
116
126
|
INLINE_PAYLOAD_NEEDLE,
|
|
117
127
|
isAttachmentViewMessage,
|
|
118
128
|
offloadInlinePayloads,
|
|
119
129
|
parseAttachmentInputs,
|
|
120
130
|
prepareAttachments,
|
|
121
|
-
viewAttachment,
|
|
122
131
|
type AttachmentViewSurface,
|
|
123
132
|
type MatrixMediaSource,
|
|
133
|
+
type ViewAttachmentDeps,
|
|
124
134
|
type SandboxUploadConfig,
|
|
125
135
|
DOWNLOAD_TIMEOUT_MS,
|
|
126
136
|
MAX_FILE_SIZE,
|
|
@@ -199,6 +209,7 @@ import {
|
|
|
199
209
|
} from '../sqlite/cache-config';
|
|
200
210
|
import { DoSqliteDatabase, type DoSqliteOpenOptions } from '../sqlite/database';
|
|
201
211
|
import type { TierFlushResult } from '../sqlite/do-vfs';
|
|
212
|
+
import type { AttachmentMeta } from '../sqlite/serialization';
|
|
202
213
|
import {
|
|
203
214
|
DEFAULT_TIER_HOT_BUDGET_BYTES,
|
|
204
215
|
type PageTierOptions,
|
|
@@ -272,6 +283,7 @@ import { decideBootDirty } from './boot-dirty';
|
|
|
272
283
|
import { decideFlush } from './flush-schedule';
|
|
273
284
|
import { mirrorTxnId, mirrorEventContent, RoomMirror } from './room-mirror';
|
|
274
285
|
import { prefixSpeaker } from './speaker-prefix';
|
|
286
|
+
import { assembleTurnMessages } from './turn-messages';
|
|
275
287
|
import {
|
|
276
288
|
type ObservedMessage,
|
|
277
289
|
summarizeObservedMessages,
|
|
@@ -291,8 +303,12 @@ import { createWorkStatusMiddleware } from '../core/middlewares/work-status';
|
|
|
291
303
|
import { UserPreferencesStore } from '../plugins/user-preferences/user-preferences-store';
|
|
292
304
|
import { SqliteUserKv } from '../sqlite/user-kv-store';
|
|
293
305
|
import {
|
|
306
|
+
channelHistoryIndexDebounceMs,
|
|
307
|
+
pageOfMessages,
|
|
294
308
|
SessionHistoryIndexer,
|
|
309
|
+
type HistoryEntry,
|
|
295
310
|
type HistoryMessage,
|
|
311
|
+
type HistoryPage,
|
|
296
312
|
type IndexableSession,
|
|
297
313
|
} from '../memory/session-history-indexer';
|
|
298
314
|
import {
|
|
@@ -1040,6 +1056,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1040
1056
|
private lastActionPruneAt: number | null = null;
|
|
1041
1057
|
/** Text `view_attachment` extracted, per session (`attachments/view-cache.ts`). */
|
|
1042
1058
|
private attachmentTextCache: AttachmentTextCacheStore | null = null;
|
|
1059
|
+
/** Large texts of document ingestion, in memory (a few, minutes). */
|
|
1060
|
+
private readonly ingestTextLru = new IngestTextLru();
|
|
1043
1061
|
/**
|
|
1044
1062
|
* Per-model context windows; learned limits persist in this object's KV
|
|
1045
1063
|
* storage (`ctxwin:<model>`), so a provider's rejection is remembered
|
|
@@ -2032,6 +2050,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2032
2050
|
beforeCreate: () => this.markRunsMaybeActive(),
|
|
2033
2051
|
});
|
|
2034
2052
|
|
|
2053
|
+
const channelRouting = channelRoutingConfig.parse(this.core.validatedEnv);
|
|
2035
2054
|
this.channelTurns = new ChannelTurns(liveDb, {
|
|
2036
2055
|
createSession: async (identity, markerTxnId) => {
|
|
2037
2056
|
const roomId = await this.resolveMainRoomId(identity.userDid);
|
|
@@ -2086,6 +2105,22 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2086
2105
|
);
|
|
2087
2106
|
}
|
|
2088
2107
|
},
|
|
2108
|
+
stopRuns: (runIds) => this.runs!.abortByIds(runIds),
|
|
2109
|
+
describeSessions: async (sessionIds) => {
|
|
2110
|
+
const found = await this.sessions!.summaries(sessionIds);
|
|
2111
|
+
return new Map(
|
|
2112
|
+
[...found].map(([sessionId, session]) => [
|
|
2113
|
+
sessionId,
|
|
2114
|
+
{
|
|
2115
|
+
title: session.title,
|
|
2116
|
+
createdAt: Date.parse(session.createdAt) || 0,
|
|
2117
|
+
updatedAt: Date.parse(session.lastUpdatedAt) || 0,
|
|
2118
|
+
},
|
|
2119
|
+
]),
|
|
2120
|
+
);
|
|
2121
|
+
},
|
|
2122
|
+
renameSession: (sessionId, title) =>
|
|
2123
|
+
this.sessions!.setTitle(sessionId, title),
|
|
2089
2124
|
getRun: (runId) => this.runStore!.get(runId),
|
|
2090
2125
|
getPlan: (runId) => this.runStore!.getPlan(runId),
|
|
2091
2126
|
wasPruned: (runId) => this.runStore!.wasChannelRunPruned(runId),
|
|
@@ -2118,6 +2153,24 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2118
2153
|
keepAlive: (work) => this.ctx.waitUntil(work),
|
|
2119
2154
|
pruneTombstones: (before, limit) =>
|
|
2120
2155
|
this.runStore!.pruneChannelTombstones(before, limit),
|
|
2156
|
+
routing: {
|
|
2157
|
+
mode: channelRouting.CHANNEL_SEMANTIC_ROUTING,
|
|
2158
|
+
bindings: channelRouting.CHANNEL_SEMANTIC_ROUTING_BINDINGS.split(',')
|
|
2159
|
+
.map((binding) => binding.trim())
|
|
2160
|
+
.filter(Boolean),
|
|
2161
|
+
minProbability:
|
|
2162
|
+
channelRouting.CHANNEL_SEMANTIC_ROUTING_MIN_PROBABILITY,
|
|
2163
|
+
evaluator: this.core.decisions,
|
|
2164
|
+
ownsSession: async (sessionId) => {
|
|
2165
|
+
const session = await this.sessions!.getSession(sessionId);
|
|
2166
|
+
const roomId = await this.resolveMainRoomId(userDid);
|
|
2167
|
+
return Boolean(
|
|
2168
|
+
sessionId.startsWith('$') && roomId && session?.roomId === roomId,
|
|
2169
|
+
);
|
|
2170
|
+
},
|
|
2171
|
+
observe: (observation) =>
|
|
2172
|
+
console.log(`[channel-routing] ${JSON.stringify(observation)}`),
|
|
2173
|
+
},
|
|
2121
2174
|
});
|
|
2122
2175
|
|
|
2123
2176
|
const core = this.core;
|
|
@@ -3068,54 +3121,87 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3068
3121
|
return toSummary(row);
|
|
3069
3122
|
}
|
|
3070
3123
|
|
|
3124
|
+
private async readyChannel(identity: TurnIdentity): Promise<void> {
|
|
3125
|
+
if (identity.ucanDelegation)
|
|
3126
|
+
throw new ChannelError(
|
|
3127
|
+
403,
|
|
3128
|
+
'Channel grants cannot become tool authority',
|
|
3129
|
+
'channel_grant_not_tool_authority',
|
|
3130
|
+
);
|
|
3131
|
+
// The shell (the only caller) checked the binding just before.
|
|
3132
|
+
// The 900 s margin is checked by admission, for a NEW request only
|
|
3133
|
+
// (and again as its run starts): a poll of a run that exists
|
|
3134
|
+
// answers its status whatever the delegation's state. Before a
|
|
3135
|
+
// COLD object boots, though, a delegation must be live (present,
|
|
3136
|
+
// unexpired): the boot may load the owner copy under it, and a user
|
|
3137
|
+
// without one must get `delegation_required`, not a boot failure.
|
|
3138
|
+
// Hydration reads the stored copy, then the deposited room state
|
|
3139
|
+
// (re-read when the stored copy is inside the margin).
|
|
3140
|
+
if (!this.db) {
|
|
3141
|
+
const live = (): boolean => {
|
|
3142
|
+
const held = this.delegations.get(identity.userDid);
|
|
3143
|
+
return (
|
|
3144
|
+
Boolean(held?.raw) &&
|
|
3145
|
+
(typeof held?.expiration !== 'number' ||
|
|
3146
|
+
held.expiration > Math.floor(Date.now() / 1000))
|
|
3147
|
+
);
|
|
3148
|
+
};
|
|
3149
|
+
if (!live())
|
|
3150
|
+
await this.hydrateDelegation(identity.userDid, {
|
|
3151
|
+
minRemainingSeconds: CHANNEL_DELEGATION_MIN_REMAINING_SECONDS,
|
|
3152
|
+
});
|
|
3153
|
+
if (!live()) throw delegationRequiredError();
|
|
3154
|
+
}
|
|
3155
|
+
try {
|
|
3156
|
+
await this.ready(identity);
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
// A delegation that lacks the file-storage grant over
|
|
3159
|
+
// `/.oracles` cannot load the owner copy: the user must
|
|
3160
|
+
// authorize again, exactly as without a delegation. (Boot throws
|
|
3161
|
+
// the failure as its RPC envelope, so it is parsed, not matched
|
|
3162
|
+
// by class.)
|
|
3163
|
+
if (parseOwnerCopyFailure(error)?.code === 'NO_VFS_DELEGATION')
|
|
3164
|
+
throw delegationRequiredError();
|
|
3165
|
+
throw error;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
async channelControl(
|
|
3170
|
+
identity: TurnIdentity,
|
|
3171
|
+
input: ChannelControlInput,
|
|
3172
|
+
requestHash: string,
|
|
3173
|
+
): Promise<ChannelControlOutcome> {
|
|
3174
|
+
try {
|
|
3175
|
+
await this.readyChannel(identity);
|
|
3176
|
+
return {
|
|
3177
|
+
ok: true,
|
|
3178
|
+
result: await this.channelTurns!.control(
|
|
3179
|
+
identity,
|
|
3180
|
+
input,
|
|
3181
|
+
requestHash,
|
|
3182
|
+
),
|
|
3183
|
+
};
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
const refusal =
|
|
3186
|
+
error instanceof ChannelError ? error : classifyChannelFailure(error);
|
|
3187
|
+
if (refusal)
|
|
3188
|
+
return {
|
|
3189
|
+
ok: false,
|
|
3190
|
+
status: refusal.status,
|
|
3191
|
+
code: refusal.code,
|
|
3192
|
+
message: refusal.message,
|
|
3193
|
+
};
|
|
3194
|
+
throw error;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3071
3198
|
async channelTurn(
|
|
3072
3199
|
identity: TurnIdentity,
|
|
3073
3200
|
input: ChannelTurnInput,
|
|
3074
3201
|
requestHash: string,
|
|
3075
3202
|
): Promise<ChannelTurnOutcome> {
|
|
3076
3203
|
try {
|
|
3077
|
-
|
|
3078
|
-
throw new ChannelError(
|
|
3079
|
-
403,
|
|
3080
|
-
'Channel grants cannot become tool authority',
|
|
3081
|
-
'channel_grant_not_tool_authority',
|
|
3082
|
-
);
|
|
3083
|
-
// The shell (the only caller) checked the binding just before.
|
|
3084
|
-
// The 900 s margin is checked by admission, for a NEW request only
|
|
3085
|
-
// (and again as its run starts): a poll of a run that exists
|
|
3086
|
-
// answers its status whatever the delegation's state. Before a
|
|
3087
|
-
// COLD object boots, though, a delegation must be live (present,
|
|
3088
|
-
// unexpired): the boot may load the owner copy under it, and a user
|
|
3089
|
-
// without one must get `delegation_required`, not a boot failure.
|
|
3090
|
-
// Hydration reads the stored copy, then the deposited room state
|
|
3091
|
-
// (re-read when the stored copy is inside the margin).
|
|
3092
|
-
if (!this.db) {
|
|
3093
|
-
const live = (): boolean => {
|
|
3094
|
-
const held = this.delegations.get(identity.userDid);
|
|
3095
|
-
return (
|
|
3096
|
-
Boolean(held?.raw) &&
|
|
3097
|
-
(typeof held?.expiration !== 'number' ||
|
|
3098
|
-
held.expiration > Math.floor(Date.now() / 1000))
|
|
3099
|
-
);
|
|
3100
|
-
};
|
|
3101
|
-
if (!live())
|
|
3102
|
-
await this.hydrateDelegation(identity.userDid, {
|
|
3103
|
-
minRemainingSeconds: CHANNEL_DELEGATION_MIN_REMAINING_SECONDS,
|
|
3104
|
-
});
|
|
3105
|
-
if (!live()) throw delegationRequiredError();
|
|
3106
|
-
}
|
|
3107
|
-
try {
|
|
3108
|
-
await this.ready(identity);
|
|
3109
|
-
} catch (error) {
|
|
3110
|
-
// A delegation that lacks the file-storage grant over
|
|
3111
|
-
// `/.oracles` cannot load the owner copy: the user must
|
|
3112
|
-
// authorize again, exactly as without a delegation. (Boot throws
|
|
3113
|
-
// the failure as its RPC envelope, so it is parsed, not matched
|
|
3114
|
-
// by class.)
|
|
3115
|
-
if (parseOwnerCopyFailure(error)?.code === 'NO_VFS_DELEGATION')
|
|
3116
|
-
throw delegationRequiredError();
|
|
3117
|
-
throw error;
|
|
3118
|
-
}
|
|
3204
|
+
await this.readyChannel(identity);
|
|
3119
3205
|
return {
|
|
3120
3206
|
ok: true,
|
|
3121
3207
|
result: await this.withActionOutcomes(
|
|
@@ -4507,19 +4593,64 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4507
4593
|
.map((m) => ({ type: m.type, content: visibleText(m) }));
|
|
4508
4594
|
}
|
|
4509
4595
|
|
|
4596
|
+
/**
|
|
4597
|
+
* The indexer's tail read: transcript rows after the message `after`
|
|
4598
|
+
* names (its id, re-resolved per read because rowids move when a
|
|
4599
|
+
* checkpoint rewrites the thread), paged so only the unindexed tail is
|
|
4600
|
+
* deserialized. The same messages as `historyMessages`, in its order;
|
|
4601
|
+
* null when `after` is no longer in the thread.
|
|
4602
|
+
*/
|
|
4603
|
+
private async historyPage(
|
|
4604
|
+
sessionId: string,
|
|
4605
|
+
after: string | null,
|
|
4606
|
+
limit: number,
|
|
4607
|
+
): Promise<HistoryPage | null> {
|
|
4608
|
+
const saver = this.saver!;
|
|
4609
|
+
let anchor = null;
|
|
4610
|
+
if (after !== null) {
|
|
4611
|
+
anchor = await saver.findThreadMessageAnchor(sessionId, after);
|
|
4612
|
+
if (!anchor) return null;
|
|
4613
|
+
}
|
|
4614
|
+
const pageRows = Math.max(1, limit);
|
|
4615
|
+
const entries: HistoryEntry[] = [];
|
|
4616
|
+
for (;;) {
|
|
4617
|
+
const rows = await saver.listThreadMessageRows(sessionId, {
|
|
4618
|
+
direction: 'newer',
|
|
4619
|
+
anchor,
|
|
4620
|
+
limit: pageRows,
|
|
4621
|
+
});
|
|
4622
|
+
for (const row of rows) {
|
|
4623
|
+
anchor = row.anchor;
|
|
4624
|
+
const m = row.message;
|
|
4625
|
+
if (isSummarizationMessage(m) || isAttachmentViewMessage(m)) continue;
|
|
4626
|
+
entries.push({
|
|
4627
|
+
message: { type: m.type, content: visibleText(m) },
|
|
4628
|
+
cursor: row.messageId,
|
|
4629
|
+
});
|
|
4630
|
+
}
|
|
4631
|
+
if (rows.length < pageRows) return { entries, more: false };
|
|
4632
|
+
if (entries.length >= limit) return { entries, more: true };
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4510
4636
|
/**
|
|
4511
4637
|
* Fire-and-forget session-history indexing (`SessionHistoryIndexer`).
|
|
4512
|
-
* The watermark
|
|
4513
|
-
*
|
|
4514
|
-
* `touchSession` is not used because it bumps `last_updated_at`
|
|
4515
|
-
* would reorder the session list.
|
|
4638
|
+
* The watermark is the session row's `last_processed_count` (the column
|
|
4639
|
+
* the Node runtime keeps, so a migrated file continues where it left
|
|
4640
|
+
* off); `touchSession` is not used because it bumps `last_updated_at`
|
|
4641
|
+
* and would reorder the session list. `afterQuietMs` defers the pass
|
|
4642
|
+
* until the session has been quiet that long (a finished channel run);
|
|
4643
|
+
* every other trigger indexes at once.
|
|
4516
4644
|
*
|
|
4517
4645
|
* Task-run sessions (`task:<id>`) are never indexed: a run's transcript
|
|
4518
4646
|
* is the task's instructions and its result, not something the user
|
|
4519
4647
|
* said, and a supplied-context task's source text must not become
|
|
4520
4648
|
* long-term memory that ordinary, tool-enabled turns can retrieve.
|
|
4521
4649
|
*/
|
|
4522
|
-
private scheduleHistoryIndexing(
|
|
4650
|
+
private scheduleHistoryIndexing(
|
|
4651
|
+
sessionId: string,
|
|
4652
|
+
opts: { afterQuietMs?: number } = {},
|
|
4653
|
+
): void {
|
|
4523
4654
|
if (sessionId.startsWith(TASK_SESSION_PREFIX)) return;
|
|
4524
4655
|
const ambient = this.ambient;
|
|
4525
4656
|
const userDid = this.userDid;
|
|
@@ -4547,9 +4678,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4547
4678
|
lastProcessedCount: row.lastProcessedCount ?? 0,
|
|
4548
4679
|
};
|
|
4549
4680
|
},
|
|
4550
|
-
|
|
4551
|
-
this.historySnapshots.get(id)
|
|
4552
|
-
|
|
4681
|
+
readMessages: async (id, after, limit) => {
|
|
4682
|
+
const snapshot = this.historySnapshots.get(id);
|
|
4683
|
+
return snapshot
|
|
4684
|
+
? pageOfMessages(snapshot.messages, after, limit)
|
|
4685
|
+
: this.historyPage(id, after, limit);
|
|
4686
|
+
},
|
|
4553
4687
|
setProcessedCount: async (id, count) => {
|
|
4554
4688
|
if (this.historySnapshots.has(id)) return; // deleted: nothing to advance
|
|
4555
4689
|
await this.sessions?.setLastProcessedCount(id, count);
|
|
@@ -4581,9 +4715,16 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4581
4715
|
logger: console,
|
|
4582
4716
|
});
|
|
4583
4717
|
const indexer = this.historyIndexer;
|
|
4718
|
+
// A deleted session's snapshot is its transcript's last chance: it is
|
|
4719
|
+
// indexed even while the session backs off after a failure.
|
|
4720
|
+
const work =
|
|
4721
|
+
opts.afterQuietMs !== undefined
|
|
4722
|
+
? indexer.processAfterQuiet(sessionId, opts.afterQuietMs)
|
|
4723
|
+
: this.historySnapshots.has(sessionId)
|
|
4724
|
+
? indexer.process(sessionId, { ignoreBackoff: true })
|
|
4725
|
+
: indexer.process(sessionId);
|
|
4584
4726
|
this.ctx.waitUntil(
|
|
4585
|
-
|
|
4586
|
-
.process(sessionId)
|
|
4727
|
+
work
|
|
4587
4728
|
.catch((err: unknown) => {
|
|
4588
4729
|
console.error(
|
|
4589
4730
|
`[user-do] session-history indexing failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -4657,11 +4798,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4657
4798
|
|
|
4658
4799
|
/**
|
|
4659
4800
|
* Start a turn's user-context prefetch — stale-while-revalidate, as the
|
|
4660
|
-
* Node AgentBuilder does it. A thread
|
|
4661
|
-
*
|
|
4662
|
-
*
|
|
4663
|
-
* room
|
|
4664
|
-
* the
|
|
4801
|
+
* Node AgentBuilder does it. A thread whose checkpoint already holds a
|
|
4802
|
+
* gathered context never waits; it refreshes the room's cache in the
|
|
4803
|
+
* background (a no-op while the cache is fresh), so the NEXT session in
|
|
4804
|
+
* the room starts from it. A channel thread also takes the cached answer
|
|
4805
|
+
* when the cache is warm (indexed channel history invalidates it, so a
|
|
4806
|
+
* later turn picks up the refresh); other clients never take a newer
|
|
4807
|
+
* answer (first-contact policy). First contact — including
|
|
4665
4808
|
* a thread whose earlier first turn got nothing (timeout, failure, skip)
|
|
4666
4809
|
* — waits for the gather, at most the cap. Returns the step the build
|
|
4667
4810
|
* awaits right before the agent is built, or null where this turn
|
|
@@ -4726,8 +4869,19 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4726
4869
|
}),
|
|
4727
4870
|
);
|
|
4728
4871
|
if (hasUserContext(input.prior)) {
|
|
4872
|
+
// A channel session is one long conversation, so it takes a newer
|
|
4873
|
+
// room answer — but only one already in the cache: an upload that
|
|
4874
|
+
// just invalidated the room must not make the next message wait for
|
|
4875
|
+
// a gather. On a miss the thread keeps its context and the
|
|
4876
|
+
// (single-flight) refresh warms the cache for a later turn.
|
|
4877
|
+
const warm =
|
|
4878
|
+
input.client === 'channel'
|
|
4879
|
+
? fetcher.readCached(input.roomId)
|
|
4880
|
+
: undefined;
|
|
4729
4881
|
keepGathering();
|
|
4730
|
-
return null;
|
|
4882
|
+
if (!warm) return null;
|
|
4883
|
+
const context = withEverySlot(warm.context);
|
|
4884
|
+
return async () => context;
|
|
4731
4885
|
}
|
|
4732
4886
|
const pending = fetcher
|
|
4733
4887
|
.fetch(params)
|
|
@@ -6095,6 +6249,16 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6095
6249
|
* input whatever its disposition.
|
|
6096
6250
|
*/
|
|
6097
6251
|
private runRequestJson(req: TurnRequest, stored: StoredRunRequest): string {
|
|
6252
|
+
if (req.client === 'channel' && req.channel?.routingClarification)
|
|
6253
|
+
return JSON.stringify({
|
|
6254
|
+
...stored,
|
|
6255
|
+
disposition: {
|
|
6256
|
+
kind: 'direct-read',
|
|
6257
|
+
text: req.channel.routingClarification,
|
|
6258
|
+
title: 'Conversation clarification',
|
|
6259
|
+
messageId: `direct:${req.sessionId}:${req.requestId}:ai`,
|
|
6260
|
+
},
|
|
6261
|
+
});
|
|
6098
6262
|
const admissible =
|
|
6099
6263
|
admissionApplies(req) &&
|
|
6100
6264
|
this.core.plugins.some((plugin) => plugin.getRequestAdmission);
|
|
@@ -6117,6 +6281,14 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6117
6281
|
const stored = JSON.parse(live.record.request) as StoredRunRequest;
|
|
6118
6282
|
const req = stored.turn;
|
|
6119
6283
|
if (req.client === 'channel') {
|
|
6284
|
+
if (
|
|
6285
|
+
req.channel &&
|
|
6286
|
+
(await this.channelTurns!.isCancelled({
|
|
6287
|
+
bindingId: req.channel.bindingId,
|
|
6288
|
+
requestId: req.requestId,
|
|
6289
|
+
}))
|
|
6290
|
+
)
|
|
6291
|
+
return { status: 'aborted', text: '' };
|
|
6120
6292
|
await this.ctx.storage.sync();
|
|
6121
6293
|
// The binding was checked as the request was admitted; a queued or
|
|
6122
6294
|
// recovered attempt starts later and checks it again.
|
|
@@ -6443,6 +6615,14 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6443
6615
|
timestamp: new Date().toISOString(),
|
|
6444
6616
|
oracleName: this.core.identity.name,
|
|
6445
6617
|
msgFromMatrixRoom: req.client === 'matrix',
|
|
6618
|
+
...(req.client === 'channel' && req.channel
|
|
6619
|
+
? {
|
|
6620
|
+
'org.ixo.qi.origin': channelOrigin(req.channel),
|
|
6621
|
+
...(req.channel.replyTo
|
|
6622
|
+
? { 'org.ixo.qi.reply': req.channel.replyTo }
|
|
6623
|
+
: {}),
|
|
6624
|
+
}
|
|
6625
|
+
: {}),
|
|
6446
6626
|
...(req.client === 'matrix'
|
|
6447
6627
|
? {
|
|
6448
6628
|
senderDid: req.identity.userDid,
|
|
@@ -6565,7 +6745,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6565
6745
|
// into the user's oracle room in the background: never awaited here, so
|
|
6566
6746
|
// the session's next queued run starts at once. Without the handle (a
|
|
6567
6747
|
// reset dropped it mid-run) the next poll or alarm mirrors it.
|
|
6568
|
-
if (record.client === 'channel')
|
|
6748
|
+
if (record.client === 'channel') {
|
|
6749
|
+
this.channelTurns?.deliverReply();
|
|
6750
|
+
// Indexed once the session has been quiet for a while, not per turn:
|
|
6751
|
+
// each upload invalidates the room's prefetch cache.
|
|
6752
|
+
if (outcome.status === 'finished')
|
|
6753
|
+
this.scheduleHistoryIndexing(record.sessionId, {
|
|
6754
|
+
afterQuietMs: channelHistoryIndexDebounceMs(this.core.validatedEnv),
|
|
6755
|
+
});
|
|
6756
|
+
}
|
|
6569
6757
|
if (!record.taskRunId || !this.taskScheduler) return;
|
|
6570
6758
|
if (this.taskScheduler.isRunActive(record.taskRunId)) return;
|
|
6571
6759
|
if (outcome.status === 'finished')
|
|
@@ -6877,54 +7065,34 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6877
7065
|
ambientLlm: LlmAdapter;
|
|
6878
7066
|
platform: OpenRouterLlmAdapter;
|
|
6879
7067
|
signal: AbortSignal;
|
|
7068
|
+
/** Attachments of the turn now running (not necessarily persisted yet). */
|
|
7069
|
+
turnMetas: readonly AttachmentMeta[];
|
|
6880
7070
|
}): Promise<AttachmentViewSurface> {
|
|
6881
7071
|
const saver = this.saver!;
|
|
6882
7072
|
const offloaded = await saver.hasThreadMessageMatching(input.sessionId, {
|
|
6883
7073
|
messageType: 'human',
|
|
6884
7074
|
contentContains: ATTACHMENT_PLACEHOLDER_PREFIX,
|
|
6885
7075
|
});
|
|
6886
|
-
const findMeta =
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
};
|
|
6902
|
-
return {
|
|
7076
|
+
const findMeta = (ref: string) =>
|
|
7077
|
+
findPersistedAttachmentMeta(saver, input.sessionId, ref);
|
|
7078
|
+
const viewDeps = (): ViewAttachmentDeps => ({
|
|
7079
|
+
source: this.matrixMediaSource(),
|
|
7080
|
+
extraction: this.extractionProvider(input.ambientLlm, input.platform),
|
|
7081
|
+
caps: getModelCapabilities(input.model),
|
|
7082
|
+
roomId: input.roomId,
|
|
7083
|
+
signal: input.signal,
|
|
7084
|
+
logger: console,
|
|
7085
|
+
...(this.attachmentTextCache
|
|
7086
|
+
? { cache: this.attachmentTextCache.forSession(input.sessionId) }
|
|
7087
|
+
: {}),
|
|
7088
|
+
ingestCache: this.ingestTextLru.forSession(input.sessionId),
|
|
7089
|
+
});
|
|
7090
|
+
return createAttachmentViewSurface({
|
|
6903
7091
|
offloaded,
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
`no attachment with ref "${ref}" in this conversation`,
|
|
6909
|
-
);
|
|
6910
|
-
}
|
|
6911
|
-
const view = await viewAttachment(meta, {
|
|
6912
|
-
source: this.matrixMediaSource(),
|
|
6913
|
-
extraction: this.extractionProvider(
|
|
6914
|
-
input.ambientLlm,
|
|
6915
|
-
input.platform,
|
|
6916
|
-
),
|
|
6917
|
-
caps: getModelCapabilities(input.model),
|
|
6918
|
-
roomId: input.roomId,
|
|
6919
|
-
signal: input.signal,
|
|
6920
|
-
logger: console,
|
|
6921
|
-
...(this.attachmentTextCache
|
|
6922
|
-
? { cache: this.attachmentTextCache.forSession(input.sessionId) }
|
|
6923
|
-
: {}),
|
|
6924
|
-
});
|
|
6925
|
-
return { meta, view };
|
|
6926
|
-
},
|
|
6927
|
-
};
|
|
7092
|
+
turnMetas: input.turnMetas,
|
|
7093
|
+
findPersisted: findMeta,
|
|
7094
|
+
deps: viewDeps,
|
|
7095
|
+
});
|
|
6928
7096
|
}
|
|
6929
7097
|
|
|
6930
7098
|
/**
|
|
@@ -7300,6 +7468,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7300
7468
|
ambientLlm: baseAmbient.llm,
|
|
7301
7469
|
platform: core.llm,
|
|
7302
7470
|
signal: abortController.signal,
|
|
7471
|
+
turnMetas: prepared?.metas ?? [],
|
|
7303
7472
|
});
|
|
7304
7473
|
|
|
7305
7474
|
// Request metadata (editor room, space, session run, entity) → state,
|
|
@@ -7696,37 +7865,43 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7696
7865
|
const timeNote = suppliedContextOnly
|
|
7697
7866
|
? undefined
|
|
7698
7867
|
: renderTurnTimeNote(turnAt, req.identity.timezone);
|
|
7699
|
-
const
|
|
7868
|
+
const replyNote = channelReplyNote(req.channel?.replyContext);
|
|
7869
|
+
const referenced = replyNote
|
|
7870
|
+
? withTurnTimeNote(spoken, replyNote)
|
|
7871
|
+
: spoken;
|
|
7872
|
+
const content = timeNote
|
|
7873
|
+
? withTurnTimeNote(referenced, timeNote)
|
|
7874
|
+
: referenced;
|
|
7875
|
+
const human = new HumanMessage({
|
|
7876
|
+
content,
|
|
7877
|
+
additional_kwargs: {
|
|
7878
|
+
timestamp: turnAt.toISOString(),
|
|
7879
|
+
...(timeNote ? { [TURN_TIME_NOTE_KWARG]: timeNote } : {}),
|
|
7880
|
+
...(replyNote ? { [CHANNEL_REPLY_NOTE_KWARG]: replyNote } : {}),
|
|
7881
|
+
oracleName: core.identity.name,
|
|
7882
|
+
msgFromMatrixRoom: req.client === 'matrix',
|
|
7883
|
+
...speakerKwargs,
|
|
7884
|
+
...(req.channel
|
|
7885
|
+
? {
|
|
7886
|
+
'org.ixo.qi.origin': channelOrigin(req.channel),
|
|
7887
|
+
...(req.channel.replyTo
|
|
7888
|
+
? { 'org.ixo.qi.reply': req.channel.replyTo }
|
|
7889
|
+
: {}),
|
|
7890
|
+
}
|
|
7891
|
+
: {}),
|
|
7892
|
+
...attachmentKwargs,
|
|
7893
|
+
},
|
|
7894
|
+
});
|
|
7700
7895
|
const stateInput = {
|
|
7701
|
-
messages
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
...(req.channel
|
|
7711
|
-
? { 'org.ixo.qi.origin': channelOrigin(req.channel) }
|
|
7712
|
-
: {}),
|
|
7713
|
-
...attachmentKwargs,
|
|
7714
|
-
},
|
|
7715
|
-
}),
|
|
7716
|
-
// Extraction-lane text rides as hidden context messages (the
|
|
7717
|
-
// transcript hides AI messages that carry an `attachment`), as on Node.
|
|
7718
|
-
...(prepared?.extracted ?? []).map(
|
|
7719
|
-
(item) =>
|
|
7720
|
-
new AIMessage({
|
|
7721
|
-
content: item.text,
|
|
7722
|
-
additional_kwargs: {
|
|
7723
|
-
msgFromMatrixRoom: req.client === 'matrix',
|
|
7724
|
-
timestamp: new Date().toISOString(),
|
|
7725
|
-
attachment: item.meta,
|
|
7726
|
-
},
|
|
7727
|
-
}),
|
|
7728
|
-
),
|
|
7729
|
-
],
|
|
7896
|
+
// Hidden attachment context (the transcript hides AI messages that
|
|
7897
|
+
// carry an `attachment`): native-lane notes precede the person's
|
|
7898
|
+
// message, extraction-lane text follows it, as on Node.
|
|
7899
|
+
messages: assembleTurnMessages({
|
|
7900
|
+
human,
|
|
7901
|
+
extracted: prepared?.extracted ?? [],
|
|
7902
|
+
nativeNotes: prepared?.nativeNotes ?? [],
|
|
7903
|
+
matrixRoom: req.client === 'matrix',
|
|
7904
|
+
}),
|
|
7730
7905
|
config: { did: req.identity.userDid },
|
|
7731
7906
|
client: req.client,
|
|
7732
7907
|
// First contact's gathered context is checkpointed with the turn,
|
|
@@ -7834,7 +8009,18 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7834
8009
|
txnId: req.channel
|
|
7835
8010
|
? `channel-${await channelRequestHash(JSON.stringify([req.identity.userDid, req.channel.bindingId, req.requestId, who]))}`
|
|
7836
8011
|
: mirrorTxnId(req.sessionId, req.requestId, who),
|
|
7837
|
-
...(req.channel
|
|
8012
|
+
...(req.channel
|
|
8013
|
+
? {
|
|
8014
|
+
origin: {
|
|
8015
|
+
provider: req.channel.provider,
|
|
8016
|
+
bindingId: req.channel.bindingId,
|
|
8017
|
+
remoteMessageRef: req.channel.remoteMessageRef,
|
|
8018
|
+
...(who === 'user' && req.channel.replyTo
|
|
8019
|
+
? { replyTo: req.channel.replyTo }
|
|
8020
|
+
: {}),
|
|
8021
|
+
},
|
|
8022
|
+
}
|
|
8023
|
+
: {}),
|
|
7838
8024
|
};
|
|
7839
8025
|
},
|
|
7840
8026
|
label,
|