@ixo/oracle-runtime-workers 0.26.0 → 0.28.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/README.md +1 -0
- package/package.json +1 -1
- 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/domain-context/turn-context.ts +1 -1
- package/src/core/env.ts +22 -4
- package/src/core/main-agent.ts +31 -5
- package/src/core/prompt-composer.ts +14 -0
- package/src/core/sub-agent-fallback.ts +3 -2
- package/src/core/user-identity.ts +67 -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/user-oracle-do.ts +269 -63
- 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 +2 -2
- package/src/plugins/action-proposals/evidence.ts +7 -8
- package/src/plugins/hub-actions/hub-actions.plugin.ts +6 -2
- package/src/plugins/index.ts +6 -2
- package/src/plugins/ixo-chain/blocksync.ts +214 -0
- package/src/plugins/ixo-chain/denoms.ts +189 -0
- package/src/plugins/ixo-chain/http.ts +388 -0
- package/src/plugins/ixo-chain/index.ts +1 -0
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +543 -0
- package/src/plugins/ixo-chain/queries.ts +401 -0
- package/src/plugins/ixo-chain/rest.ts +79 -0
- package/src/plugins/ixo-chain/summaries.ts +799 -0
- package/src/plugins/tasks/middleware.ts +4 -9
- 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';
|
|
@@ -294,8 +303,12 @@ import { createWorkStatusMiddleware } from '../core/middlewares/work-status';
|
|
|
294
303
|
import { UserPreferencesStore } from '../plugins/user-preferences/user-preferences-store';
|
|
295
304
|
import { SqliteUserKv } from '../sqlite/user-kv-store';
|
|
296
305
|
import {
|
|
306
|
+
channelHistoryIndexDebounceMs,
|
|
307
|
+
pageOfMessages,
|
|
297
308
|
SessionHistoryIndexer,
|
|
309
|
+
type HistoryEntry,
|
|
298
310
|
type HistoryMessage,
|
|
311
|
+
type HistoryPage,
|
|
299
312
|
type IndexableSession,
|
|
300
313
|
} from '../memory/session-history-indexer';
|
|
301
314
|
import {
|
|
@@ -2037,6 +2050,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2037
2050
|
beforeCreate: () => this.markRunsMaybeActive(),
|
|
2038
2051
|
});
|
|
2039
2052
|
|
|
2053
|
+
const channelRouting = channelRoutingConfig.parse(this.core.validatedEnv);
|
|
2040
2054
|
this.channelTurns = new ChannelTurns(liveDb, {
|
|
2041
2055
|
createSession: async (identity, markerTxnId) => {
|
|
2042
2056
|
const roomId = await this.resolveMainRoomId(identity.userDid);
|
|
@@ -2091,6 +2105,22 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2091
2105
|
);
|
|
2092
2106
|
}
|
|
2093
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),
|
|
2094
2124
|
getRun: (runId) => this.runStore!.get(runId),
|
|
2095
2125
|
getPlan: (runId) => this.runStore!.getPlan(runId),
|
|
2096
2126
|
wasPruned: (runId) => this.runStore!.wasChannelRunPruned(runId),
|
|
@@ -2123,6 +2153,24 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2123
2153
|
keepAlive: (work) => this.ctx.waitUntil(work),
|
|
2124
2154
|
pruneTombstones: (before, limit) =>
|
|
2125
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
|
+
},
|
|
2126
2174
|
});
|
|
2127
2175
|
|
|
2128
2176
|
const core = this.core;
|
|
@@ -3073,54 +3121,87 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3073
3121
|
return toSummary(row);
|
|
3074
3122
|
}
|
|
3075
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
|
+
|
|
3076
3198
|
async channelTurn(
|
|
3077
3199
|
identity: TurnIdentity,
|
|
3078
3200
|
input: ChannelTurnInput,
|
|
3079
3201
|
requestHash: string,
|
|
3080
3202
|
): Promise<ChannelTurnOutcome> {
|
|
3081
3203
|
try {
|
|
3082
|
-
|
|
3083
|
-
throw new ChannelError(
|
|
3084
|
-
403,
|
|
3085
|
-
'Channel grants cannot become tool authority',
|
|
3086
|
-
'channel_grant_not_tool_authority',
|
|
3087
|
-
);
|
|
3088
|
-
// The shell (the only caller) checked the binding just before.
|
|
3089
|
-
// The 900 s margin is checked by admission, for a NEW request only
|
|
3090
|
-
// (and again as its run starts): a poll of a run that exists
|
|
3091
|
-
// answers its status whatever the delegation's state. Before a
|
|
3092
|
-
// COLD object boots, though, a delegation must be live (present,
|
|
3093
|
-
// unexpired): the boot may load the owner copy under it, and a user
|
|
3094
|
-
// without one must get `delegation_required`, not a boot failure.
|
|
3095
|
-
// Hydration reads the stored copy, then the deposited room state
|
|
3096
|
-
// (re-read when the stored copy is inside the margin).
|
|
3097
|
-
if (!this.db) {
|
|
3098
|
-
const live = (): boolean => {
|
|
3099
|
-
const held = this.delegations.get(identity.userDid);
|
|
3100
|
-
return (
|
|
3101
|
-
Boolean(held?.raw) &&
|
|
3102
|
-
(typeof held?.expiration !== 'number' ||
|
|
3103
|
-
held.expiration > Math.floor(Date.now() / 1000))
|
|
3104
|
-
);
|
|
3105
|
-
};
|
|
3106
|
-
if (!live())
|
|
3107
|
-
await this.hydrateDelegation(identity.userDid, {
|
|
3108
|
-
minRemainingSeconds: CHANNEL_DELEGATION_MIN_REMAINING_SECONDS,
|
|
3109
|
-
});
|
|
3110
|
-
if (!live()) throw delegationRequiredError();
|
|
3111
|
-
}
|
|
3112
|
-
try {
|
|
3113
|
-
await this.ready(identity);
|
|
3114
|
-
} catch (error) {
|
|
3115
|
-
// A delegation that lacks the file-storage grant over
|
|
3116
|
-
// `/.oracles` cannot load the owner copy: the user must
|
|
3117
|
-
// authorize again, exactly as without a delegation. (Boot throws
|
|
3118
|
-
// the failure as its RPC envelope, so it is parsed, not matched
|
|
3119
|
-
// by class.)
|
|
3120
|
-
if (parseOwnerCopyFailure(error)?.code === 'NO_VFS_DELEGATION')
|
|
3121
|
-
throw delegationRequiredError();
|
|
3122
|
-
throw error;
|
|
3123
|
-
}
|
|
3204
|
+
await this.readyChannel(identity);
|
|
3124
3205
|
return {
|
|
3125
3206
|
ok: true,
|
|
3126
3207
|
result: await this.withActionOutcomes(
|
|
@@ -4512,19 +4593,64 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4512
4593
|
.map((m) => ({ type: m.type, content: visibleText(m) }));
|
|
4513
4594
|
}
|
|
4514
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
|
+
|
|
4515
4636
|
/**
|
|
4516
4637
|
* Fire-and-forget session-history indexing (`SessionHistoryIndexer`).
|
|
4517
|
-
* The watermark
|
|
4518
|
-
*
|
|
4519
|
-
* `touchSession` is not used because it bumps `last_updated_at`
|
|
4520
|
-
* 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.
|
|
4521
4644
|
*
|
|
4522
4645
|
* Task-run sessions (`task:<id>`) are never indexed: a run's transcript
|
|
4523
4646
|
* is the task's instructions and its result, not something the user
|
|
4524
4647
|
* said, and a supplied-context task's source text must not become
|
|
4525
4648
|
* long-term memory that ordinary, tool-enabled turns can retrieve.
|
|
4526
4649
|
*/
|
|
4527
|
-
private scheduleHistoryIndexing(
|
|
4650
|
+
private scheduleHistoryIndexing(
|
|
4651
|
+
sessionId: string,
|
|
4652
|
+
opts: { afterQuietMs?: number } = {},
|
|
4653
|
+
): void {
|
|
4528
4654
|
if (sessionId.startsWith(TASK_SESSION_PREFIX)) return;
|
|
4529
4655
|
const ambient = this.ambient;
|
|
4530
4656
|
const userDid = this.userDid;
|
|
@@ -4552,9 +4678,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4552
4678
|
lastProcessedCount: row.lastProcessedCount ?? 0,
|
|
4553
4679
|
};
|
|
4554
4680
|
},
|
|
4555
|
-
|
|
4556
|
-
this.historySnapshots.get(id)
|
|
4557
|
-
|
|
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
|
+
},
|
|
4558
4687
|
setProcessedCount: async (id, count) => {
|
|
4559
4688
|
if (this.historySnapshots.has(id)) return; // deleted: nothing to advance
|
|
4560
4689
|
await this.sessions?.setLastProcessedCount(id, count);
|
|
@@ -4586,9 +4715,16 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4586
4715
|
logger: console,
|
|
4587
4716
|
});
|
|
4588
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);
|
|
4589
4726
|
this.ctx.waitUntil(
|
|
4590
|
-
|
|
4591
|
-
.process(sessionId)
|
|
4727
|
+
work
|
|
4592
4728
|
.catch((err: unknown) => {
|
|
4593
4729
|
console.error(
|
|
4594
4730
|
`[user-do] session-history indexing failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -4662,11 +4798,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4662
4798
|
|
|
4663
4799
|
/**
|
|
4664
4800
|
* Start a turn's user-context prefetch — stale-while-revalidate, as the
|
|
4665
|
-
* Node AgentBuilder does it. A thread
|
|
4666
|
-
*
|
|
4667
|
-
*
|
|
4668
|
-
* room
|
|
4669
|
-
* 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
|
|
4670
4808
|
* a thread whose earlier first turn got nothing (timeout, failure, skip)
|
|
4671
4809
|
* — waits for the gather, at most the cap. Returns the step the build
|
|
4672
4810
|
* awaits right before the agent is built, or null where this turn
|
|
@@ -4731,8 +4869,19 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4731
4869
|
}),
|
|
4732
4870
|
);
|
|
4733
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;
|
|
4734
4881
|
keepGathering();
|
|
4735
|
-
return null;
|
|
4882
|
+
if (!warm) return null;
|
|
4883
|
+
const context = withEverySlot(warm.context);
|
|
4884
|
+
return async () => context;
|
|
4736
4885
|
}
|
|
4737
4886
|
const pending = fetcher
|
|
4738
4887
|
.fetch(params)
|
|
@@ -6100,6 +6249,16 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6100
6249
|
* input whatever its disposition.
|
|
6101
6250
|
*/
|
|
6102
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
|
+
});
|
|
6103
6262
|
const admissible =
|
|
6104
6263
|
admissionApplies(req) &&
|
|
6105
6264
|
this.core.plugins.some((plugin) => plugin.getRequestAdmission);
|
|
@@ -6122,6 +6281,14 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6122
6281
|
const stored = JSON.parse(live.record.request) as StoredRunRequest;
|
|
6123
6282
|
const req = stored.turn;
|
|
6124
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: '' };
|
|
6125
6292
|
await this.ctx.storage.sync();
|
|
6126
6293
|
// The binding was checked as the request was admitted; a queued or
|
|
6127
6294
|
// recovered attempt starts later and checks it again.
|
|
@@ -6448,6 +6615,14 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6448
6615
|
timestamp: new Date().toISOString(),
|
|
6449
6616
|
oracleName: this.core.identity.name,
|
|
6450
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
|
+
: {}),
|
|
6451
6626
|
...(req.client === 'matrix'
|
|
6452
6627
|
? {
|
|
6453
6628
|
senderDid: req.identity.userDid,
|
|
@@ -6570,7 +6745,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
6570
6745
|
// into the user's oracle room in the background: never awaited here, so
|
|
6571
6746
|
// the session's next queued run starts at once. Without the handle (a
|
|
6572
6747
|
// reset dropped it mid-run) the next poll or alarm mirrors it.
|
|
6573
|
-
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
|
+
}
|
|
6574
6757
|
if (!record.taskRunId || !this.taskScheduler) return;
|
|
6575
6758
|
if (this.taskScheduler.isRunActive(record.taskRunId)) return;
|
|
6576
6759
|
if (outcome.status === 'finished')
|
|
@@ -7682,17 +7865,29 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7682
7865
|
const timeNote = suppliedContextOnly
|
|
7683
7866
|
? undefined
|
|
7684
7867
|
: renderTurnTimeNote(turnAt, req.identity.timezone);
|
|
7685
|
-
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;
|
|
7686
7875
|
const human = new HumanMessage({
|
|
7687
7876
|
content,
|
|
7688
7877
|
additional_kwargs: {
|
|
7689
7878
|
timestamp: turnAt.toISOString(),
|
|
7690
7879
|
...(timeNote ? { [TURN_TIME_NOTE_KWARG]: timeNote } : {}),
|
|
7880
|
+
...(replyNote ? { [CHANNEL_REPLY_NOTE_KWARG]: replyNote } : {}),
|
|
7691
7881
|
oracleName: core.identity.name,
|
|
7692
7882
|
msgFromMatrixRoom: req.client === 'matrix',
|
|
7693
7883
|
...speakerKwargs,
|
|
7694
7884
|
...(req.channel
|
|
7695
|
-
? {
|
|
7885
|
+
? {
|
|
7886
|
+
'org.ixo.qi.origin': channelOrigin(req.channel),
|
|
7887
|
+
...(req.channel.replyTo
|
|
7888
|
+
? { 'org.ixo.qi.reply': req.channel.replyTo }
|
|
7889
|
+
: {}),
|
|
7890
|
+
}
|
|
7696
7891
|
: {}),
|
|
7697
7892
|
...attachmentKwargs,
|
|
7698
7893
|
},
|
|
@@ -7814,7 +8009,18 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
7814
8009
|
txnId: req.channel
|
|
7815
8010
|
? `channel-${await channelRequestHash(JSON.stringify([req.identity.userDid, req.channel.bindingId, req.requestId, who]))}`
|
|
7816
8011
|
: mirrorTxnId(req.sessionId, req.requestId, who),
|
|
7817
|
-
...(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
|
+
: {}),
|
|
7818
8024
|
};
|
|
7819
8025
|
},
|
|
7820
8026
|
label,
|