letagents 0.12.19 → 0.12.21
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/dist/mcp/local-state/local-chat.js +6 -1
- package/dist/mcp/server/register-tools.js +2 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +5 -1
- package/dist/mcp/server/runtime/worker-bearer.js +3 -0
- package/dist/mcp/server/supervised-tool-facade.js +1 -0
- package/dist/mcp/server/tools/knowledge.js +76 -0
- package/dist/mcp/server/tools/rooms/inspection-tools.js +1 -0
- package/dist/shared/activation-routing.js +2 -468
- package/dist/shared/room-agent-prompts.js +1 -1
- package/package.json +2 -2
- package/shared/activation-routing.d.mts +139 -0
- package/shared/activation-routing.mjs +468 -0
- package/shared/local-room-knowledge.d.mts +7 -0
- package/shared/local-room-knowledge.mjs +53 -0
- package/shared/local-supervised-routing.d.mts +9 -0
- package/shared/local-supervised-routing.mjs +93 -0
- package/shared/message-contracts.d.mts +2 -0
- package/shared/message-contracts.mjs +11 -0
- package/shared/room-api-origin.d.mts +3 -0
- package/shared/room-api-origin.mjs +12 -0
- package/shared/room-knowledge.d.mts +25 -0
- package/shared/room-knowledge.mjs +74 -0
- package/shared/sqlite-thread-routing.d.mts +4 -0
- package/shared/sqlite-thread-routing.mjs +30 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SqliteRoutingDatabase } from "./sqlite-thread-routing.mjs";
|
|
2
|
+
export type LocalSupervisedMessageRow = {
|
|
3
|
+
room_id: string; number: number; sender: string; text: string; source: string | null;
|
|
4
|
+
publisher_agent_key: string | null; thread_root_number: number | null; reply_to_number: number | null;
|
|
5
|
+
control_authorized: number | null; timestamp: string; sync_key: string | null;
|
|
6
|
+
};
|
|
7
|
+
export function ensureLocalSupervisedRoutingSchema(db: SqliteRoutingDatabase): void;
|
|
8
|
+
export function captureLocalSupervisedRouting(db: SqliteRoutingDatabase, row: LocalSupervisedMessageRow): void;
|
|
9
|
+
export function runLocalSupervisedMessageWrite<T>(db: SqliteRoutingDatabase, roomId: string, threadRootNumber: number | null, work: () => T): Promise<T>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createGlobalAgentAddressResolver, decideAgentMessageActivation, humanConversationFallback } from "./activation-routing.mjs";
|
|
2
|
+
import { readProjectedLocalThreadRoutingAgentKeys, ensureRequestedRootsProjected, runLocalSqliteWriteTransactionAsync, LocalThreadRoutingProjectionChangedError } from "./sqlite-thread-routing.mjs";
|
|
3
|
+
import { parseSupervisedReplySourceNumber } from "./message-contracts.mjs";
|
|
4
|
+
export function ensureLocalSupervisedRoutingSchema(db) {
|
|
5
|
+
db.exec(`CREATE TABLE IF NOT EXISTS local_supervisor_message_routes (
|
|
6
|
+
room_id TEXT NOT NULL, message_id TEXT NOT NULL, routes_json TEXT NOT NULL,
|
|
7
|
+
PRIMARY KEY(room_id, message_id)
|
|
8
|
+
) STRICT`);
|
|
9
|
+
}
|
|
10
|
+
function recentRecipient(db, message) {
|
|
11
|
+
const recent = db.prepare(`SELECT * FROM local_chat_messages WHERE room_id=? AND number<?
|
|
12
|
+
ORDER BY number DESC LIMIT 50`).all(message.room_id, message.number)
|
|
13
|
+
.filter(row => Date.parse(String(row.timestamp)) >= Date.parse(message.timestamp) - 30 * 60_000);
|
|
14
|
+
const human = recent.find(row => row.source === "browser" && !row.publisher_agent_key
|
|
15
|
+
&& row.control_authorized === 1 && row.thread_root_number === null);
|
|
16
|
+
if (!human)
|
|
17
|
+
return null;
|
|
18
|
+
const captured = db.prepare("SELECT routes_json FROM local_supervisor_message_routes WHERE room_id=? AND message_id=?")
|
|
19
|
+
.get(message.room_id, `msg_${human.number}`);
|
|
20
|
+
if (!captured)
|
|
21
|
+
return null;
|
|
22
|
+
const recipients = Object.entries(JSON.parse(String(captured.routes_json)))
|
|
23
|
+
.filter(([, route]) => route.decision === "activate").map(([key]) => key);
|
|
24
|
+
if (recipients.length === 1)
|
|
25
|
+
return recipients[0];
|
|
26
|
+
const answered = new Set();
|
|
27
|
+
for (const reply of recent) {
|
|
28
|
+
const key = String(reply.publisher_agent_key || "");
|
|
29
|
+
const prefix = `local-supervised:${key}:`;
|
|
30
|
+
const receipt = String(reply.sync_key || "");
|
|
31
|
+
if (reply.source === "agent" && Number(reply.number) > Number(human.number)
|
|
32
|
+
&& recipients.includes(key) && (reply.reply_to_number === human.number
|
|
33
|
+
|| (receipt.startsWith(prefix)
|
|
34
|
+
&& parseSupervisedReplySourceNumber(receipt.slice(prefix.length)) === human.number)))
|
|
35
|
+
answered.add(key);
|
|
36
|
+
}
|
|
37
|
+
return answered.size === 1 ? [...answered][0] : null;
|
|
38
|
+
}
|
|
39
|
+
/** Capture all recipients atomically with the message, including the empty-room case. */
|
|
40
|
+
export function captureLocalSupervisedRouting(db, row) {
|
|
41
|
+
const reply = row.reply_to_number ? db.prepare("SELECT * FROM local_chat_messages WHERE room_id=? AND number=?")
|
|
42
|
+
.get(row.room_id, row.reply_to_number) : null;
|
|
43
|
+
const registered = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='local_supervisor_grants'").get();
|
|
44
|
+
const identities = registered
|
|
45
|
+
? db.prepare("SELECT agent_key,display_name FROM local_supervisor_grants WHERE room_id=? AND revoked_at IS NULL ORDER BY entry_id")
|
|
46
|
+
.all(row.room_id).map(grant => ({ agent_key: String(grant.agent_key), display_name: String(grant.display_name),
|
|
47
|
+
actor_label: String(grant.display_name), agent_instance_id: null, agent_session_id: null, session_kind: "worker" })) : [];
|
|
48
|
+
const id = `msg_${row.number}`;
|
|
49
|
+
const rootId = `msg_${row.thread_root_number ?? row.number}`;
|
|
50
|
+
const threadReply = row.thread_root_number !== null && row.thread_root_number !== row.number;
|
|
51
|
+
const message = { id, text: row.text, sender: row.sender, source: row.source,
|
|
52
|
+
thread_root_id: threadReply ? rootId : null, reply_to: reply ? { id: `msg_${reply.number}`, sender: reply.sender } : null };
|
|
53
|
+
const address = createGlobalAgentAddressResolver(identities)(message);
|
|
54
|
+
const participants = threadReply ? readProjectedLocalThreadRoutingAgentKeys(db, row.room_id, [row.thread_root_number], identities.map(identity => ({ agentKey: identity.agent_key, displayName: identity.display_name, actorLabel: identity.actor_label })))
|
|
55
|
+
.get(row.thread_root_number) ?? new Set() : new Set();
|
|
56
|
+
const fallback = humanConversationFallback({ source: row.source,
|
|
57
|
+
publisherAccountId: row.control_authorized === 1 ? "local-owner" : null, publisherAgentKey: row.publisher_agent_key,
|
|
58
|
+
explicitlyAddressed: address.broadcast || address.hasAgentMention || threadReply || Boolean(reply),
|
|
59
|
+
registeredAgentKeys: identities.map(identity => identity.agent_key),
|
|
60
|
+
recentAgentKey: identities.length > 2 ? recentRecipient(db, row) : null });
|
|
61
|
+
const routes = {};
|
|
62
|
+
for (const identity of identities) {
|
|
63
|
+
const key = identity.agent_key;
|
|
64
|
+
let decision = decideAgentMessageActivation(message, identity, {
|
|
65
|
+
selfMessageIds: new Set(row.source === "agent" && row.publisher_agent_key === key ? [id] : []),
|
|
66
|
+
explicitMentionMessageIds: new Set(address.explicitMentionKeys.has(key) ? [id] : []),
|
|
67
|
+
replyTargetMessageIds: new Set(!threadReply && (reply?.publisher_agent_key
|
|
68
|
+
? reply.publisher_agent_key === key : address.replyTargetKeys.has(key)) ? [id] : []),
|
|
69
|
+
threadParticipantRootIds: new Set(participants.has(key) ? [rootId] : []),
|
|
70
|
+
});
|
|
71
|
+
if (row.source === "agent" && !row.publisher_agent_key)
|
|
72
|
+
decision = { decision: "silent", reason: "unaddressed", addressed: false };
|
|
73
|
+
if (decision.reason === "unaddressed" && fallback?.agentKeys.includes(key)) {
|
|
74
|
+
decision = { decision: "activate", reason: fallback.reason, addressed: true };
|
|
75
|
+
}
|
|
76
|
+
routes[key] = decision;
|
|
77
|
+
}
|
|
78
|
+
db.prepare("INSERT INTO local_supervisor_message_routes(room_id,message_id,routes_json) VALUES(?,?,?)")
|
|
79
|
+
.run(row.room_id, id, JSON.stringify(routes));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Repair outside the write lock; a concurrent invalidation rolls back the entire send. */
|
|
83
|
+
export async function runLocalSupervisedMessageWrite(db, roomId, threadRootNumber, work) {
|
|
84
|
+
const deadline = performance.now() + 2_000;
|
|
85
|
+
for (;;) {
|
|
86
|
+
if (threadRootNumber) await ensureRequestedRootsProjected(db, roomId, [threadRootNumber]);
|
|
87
|
+
try {
|
|
88
|
+
return await runLocalSqliteWriteTransactionAsync(db, work);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!(error instanceof LocalThreadRoutingProjectionChangedError) || performance.now() >= deadline) throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -107,3 +107,14 @@ export function parseAccountAgentRoutingEnvelope(routing) {
|
|
|
107
107
|
controlAuthorized: routing.control_authorized === true,
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
/** Only daemon publication identities can link an answer to its source receipt. */
|
|
112
|
+
export function parseSupervisedReplySourceNumber(clientMessageId) {
|
|
113
|
+
if (!clientMessageId) return null;
|
|
114
|
+
const parts = clientMessageId.split(":");
|
|
115
|
+
if (parts[0] !== "supervised-room" || parts.at(-2) !== "reply" || parts.at(-1) !== "v1") return null;
|
|
116
|
+
const body = parts.slice(1, -2);
|
|
117
|
+
if (body.length !== 2 && body.length !== 3) return null;
|
|
118
|
+
const source = body.at(-1);
|
|
119
|
+
return source ? parsePositivePgIntegerScopedId(source, "msg") : null;
|
|
120
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** An in-process storage adapter, never an HTTP endpoint or cloud credential audience. */
|
|
2
|
+
export const LOCAL_ROOM_API_ORIGIN = "letagents-local://rooms";
|
|
3
|
+
|
|
4
|
+
export function isLocalRoomApi(value) {
|
|
5
|
+
return value === LOCAL_ROOM_API_ORIGIN;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Preserve the existing HTTP normalization while retaining the explicit local authority. */
|
|
9
|
+
export function roomApiOrigin(value) {
|
|
10
|
+
if (isLocalRoomApi(value)) return LOCAL_ROOM_API_ORIGIN;
|
|
11
|
+
return new URL(value).origin;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type KnowledgeType = 'memory' | 'attention';
|
|
2
|
+
export type MemoryCategory = 'goal' | 'decision' | 'constraint' | 'term' | 'reference';
|
|
3
|
+
export type AttentionCategory = 'question' | 'decision' | 'approval' | 'review';
|
|
4
|
+
export interface KnowledgeActor { id: string; label: string; kind: 'human' | 'agent' }
|
|
5
|
+
export interface KnowledgeInput {
|
|
6
|
+
category: MemoryCategory | AttentionCategory; title: string; body: string;
|
|
7
|
+
recommendation?: string; unblocks?: string; source_url?: string; source_message_id?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface KnowledgeRecord extends Required<KnowledgeInput> {
|
|
10
|
+
id: string; room_id: string; type: KnowledgeType; version: number;
|
|
11
|
+
author: KnowledgeActor; updated_by: KnowledgeActor; created_at: string; updated_at: string; archived: boolean;
|
|
12
|
+
response: { body: string; actor: KnowledgeActor; at: string } | null;
|
|
13
|
+
}
|
|
14
|
+
export interface KnowledgePage { records: KnowledgeRecord[]; truncated: boolean }
|
|
15
|
+
export interface KnowledgeRevisionInput extends Partial<KnowledgeInput> { expected_version: number; response?: string; archived?: boolean }
|
|
16
|
+
export const MEMORY_CATEGORIES: MemoryCategory[];
|
|
17
|
+
export const ATTENTION_CATEGORIES: AttentionCategory[];
|
|
18
|
+
export class RoomKnowledgeError extends Error { status: number; constructor(message: string, status?: number) }
|
|
19
|
+
export function parseKnowledgeInput(type: KnowledgeType, value: unknown): Required<KnowledgeInput>;
|
|
20
|
+
export function knowledgeId(value: unknown): string;
|
|
21
|
+
export function createKnowledgeRecord(room_id: string, type: KnowledgeType, input: KnowledgeInput & { client_id: string }, author: KnowledgeActor, now?: string): KnowledgeRecord;
|
|
22
|
+
export function reviseKnowledgeRecord(record: KnowledgeRecord, input: KnowledgeRevisionInput, actor: KnowledgeActor, now?: string): KnowledgeRecord;
|
|
23
|
+
export function assertKnowledgeReplay(existing: KnowledgeRecord, candidate: KnowledgeRecord): void;
|
|
24
|
+
|
|
25
|
+
export function formatAttentionResponse(record: KnowledgeRecord): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const MEMORY_CATEGORIES = ['goal', 'decision', 'constraint', 'term', 'reference'];
|
|
2
|
+
export const ATTENTION_CATEGORIES = ['question', 'decision', 'approval', 'review'];
|
|
3
|
+
|
|
4
|
+
export class RoomKnowledgeError extends Error {
|
|
5
|
+
constructor(message, status = 400) { super(message); this.status = status; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function text(value, name, max, required = false) {
|
|
9
|
+
if (value == null && !required) return '';
|
|
10
|
+
if (typeof value !== 'string' || value.length > max || (required && !value.trim())) {
|
|
11
|
+
throw new RoomKnowledgeError(`${name} must be ${required ? 'nonempty text' : 'text'} of at most ${max} characters.`);
|
|
12
|
+
}
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseKnowledgeInput(type, value) {
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new RoomKnowledgeError('A record is required.');
|
|
18
|
+
const categories = type === 'memory' ? MEMORY_CATEGORIES : type === 'attention' ? ATTENTION_CATEGORIES : [];
|
|
19
|
+
if (!categories.includes(value.category)) throw new RoomKnowledgeError('Choose a valid category.');
|
|
20
|
+
const source_url = text(value.source_url, 'Source URL', 2048);
|
|
21
|
+
if (source_url) {
|
|
22
|
+
let url;
|
|
23
|
+
try { url = new URL(source_url); } catch { throw new RoomKnowledgeError('Source URL must be an HTTP or HTTPS link.'); }
|
|
24
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) throw new RoomKnowledgeError('Source URL must be an HTTP or HTTPS link without credentials.');
|
|
25
|
+
}
|
|
26
|
+
const source_message_id = text(value.source_message_id, 'Source message', 40);
|
|
27
|
+
if (source_message_id && (!/^msg_[1-9]\d{0,9}$/.test(source_message_id) || Number(source_message_id.slice(4)) > 2147483647)) throw new RoomKnowledgeError('Invalid source message.');
|
|
28
|
+
return {
|
|
29
|
+
category: value.category,
|
|
30
|
+
title: text(value.title, 'Title', 160, true),
|
|
31
|
+
body: text(value.body, 'Context', 8000, true),
|
|
32
|
+
recommendation: type === 'attention' ? text(value.recommendation, 'Recommendation', 2000) : '',
|
|
33
|
+
unblocks: type === 'attention' ? text(value.unblocks, 'What this unblocks', 1000) : '',
|
|
34
|
+
source_url,
|
|
35
|
+
source_message_id,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function knowledgeId(value) {
|
|
40
|
+
if (typeof value !== 'string' || !/^[a-zA-Z0-9_-]{8,80}$/.test(value)) throw new RoomKnowledgeError('Use a stable client ID of 8–80 letters, numbers, underscores or hyphens.');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createKnowledgeRecord(room_id, type, input, author, now = new Date().toISOString()) {
|
|
45
|
+
return { ...parseKnowledgeInput(type, input), id: knowledgeId(input.client_id), room_id, type,
|
|
46
|
+
version: 1, author, updated_by: author, created_at: now, updated_at: now,
|
|
47
|
+
archived: false, response: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function reviseKnowledgeRecord(record, input, actor, now = new Date().toISOString()) {
|
|
51
|
+
if (input.expected_version !== record.version) throw new RoomKnowledgeError('This changed since you opened it. Refresh to see the latest version.', 409);
|
|
52
|
+
if (actor.kind !== 'human') throw new RoomKnowledgeError('Only a human can revise shared memory or answer a request.', 403);
|
|
53
|
+
if (record.type === 'attention') {
|
|
54
|
+
if (record.response) throw new RoomKnowledgeError('This request has already been answered.', 409);
|
|
55
|
+
return { ...record, version: record.version + 1, updated_at: now, updated_by: actor,
|
|
56
|
+
response: { body: text(input.response, 'Response', 8000, true), actor, at: now } };
|
|
57
|
+
}
|
|
58
|
+
if (input.archived !== undefined && typeof input.archived !== 'boolean') throw new RoomKnowledgeError('Archived must be true or false.');
|
|
59
|
+
const content = input.archived !== undefined ? {} : parseKnowledgeInput('memory', input);
|
|
60
|
+
return { ...record, ...content, archived: input.archived ?? record.archived,
|
|
61
|
+
version: record.version + 1, updated_at: now, updated_by: actor };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function assertKnowledgeReplay(existing, candidate) {
|
|
65
|
+
const keys = ['type', 'category', 'title', 'body', 'recommendation', 'unblocks', 'source_url', 'source_message_id'];
|
|
66
|
+
if (existing.author.id !== candidate.author.id || existing.author.kind !== candidate.author.kind || keys.some(key => existing[key] !== candidate[key])) {
|
|
67
|
+
throw new RoomKnowledgeError('This client ID was already used for different content. Use a new ID for a new record.', 409);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatAttentionResponse(record) {
|
|
72
|
+
const handle = record.author.kind === 'agent' ? record.author.id.trim().replace(/[A-Z]/g, c => c.toLowerCase()).replace(/[^a-z0-9_.:/-]+/g, '') : '';
|
|
73
|
+
return `${handle ? `@agent:${handle}\n\n` : ''}Human response (${record.id}):\n\n${record.response.body}`;
|
|
74
|
+
}
|
|
@@ -70,3 +70,7 @@ export function getLocalThreadRoutingAgentKeysForRoots(
|
|
|
70
70
|
signal?: AbortSignal;
|
|
71
71
|
},
|
|
72
72
|
): Promise<Map<number, Set<string>>>;
|
|
73
|
+
|
|
74
|
+
export function ensureRequestedRootsProjected(database: SqliteRoutingDatabase, roomId: string, rootNumbers: readonly number[], options?: { foregroundTimeBudgetMs?: number; scheduleOnTimeout?: boolean; signal?: AbortSignal }): Promise<void>;
|
|
75
|
+
export function readProjectedLocalThreadRoutingAgentKeys(database: SqliteRoutingDatabase, roomId: string, rootNumbers: readonly number[], identities: readonly RoutingIdentityLike[]): Map<number, Set<string>>;
|
|
76
|
+
export class LocalThreadRoutingProjectionChangedError extends LocalThreadRoutingProjectionUnavailableError {}
|
|
@@ -788,7 +788,7 @@ export function invalidateLocalThreadRoutingRoots(database, roomId, rootNumbersI
|
|
|
788
788
|
}
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
-
async function ensureRequestedRootsProjected(database, roomId, rootNumbers, options = {}) {
|
|
791
|
+
export async function ensureRequestedRootsProjected(database, roomId, rootNumbers, options = {}) {
|
|
792
792
|
if (rootNumbers.length > LOCAL_THREAD_ROUTING_MAX_REQUESTED_ROOTS) {
|
|
793
793
|
throw new LocalThreadRoutingProjectionUnavailableError();
|
|
794
794
|
}
|
|
@@ -869,6 +869,33 @@ export async function getLocalThreadRoutingAgentKeysForRoots(
|
|
|
869
869
|
if (rootNumbers.length === 0 || identities.length === 0) return new Map();
|
|
870
870
|
await ensureRequestedRootsProjected(database, roomId, rootNumbers, options);
|
|
871
871
|
|
|
872
|
+
const batches = readProjectedThreadRoutingBatches(database, roomId, rootNumbers, identities);
|
|
873
|
+
for (;;) {
|
|
874
|
+
const step = batches.next();
|
|
875
|
+
if (step.done) return step.value;
|
|
876
|
+
await yieldToEventLoop();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Read prepared projections synchronously inside an existing message transaction. */
|
|
881
|
+
export function readProjectedLocalThreadRoutingAgentKeys(database, roomId, rootNumbers, identities) {
|
|
882
|
+
const statements = requestedRootProjectionStatements(database);
|
|
883
|
+
const rootsJson = JSON.stringify(rootNumbers);
|
|
884
|
+
if (pendingRequestedRoots(statements, roomId, rootsJson).length
|
|
885
|
+
|| statements.invalidated.all(roomId, rootsJson).length) {
|
|
886
|
+
throw new LocalThreadRoutingProjectionChangedError();
|
|
887
|
+
}
|
|
888
|
+
const batches = readProjectedThreadRoutingBatches(database, roomId, rootNumbers, identities);
|
|
889
|
+
for (;;) {
|
|
890
|
+
const step = batches.next();
|
|
891
|
+
if (step.done) return step.value;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export class LocalThreadRoutingProjectionChangedError extends LocalThreadRoutingProjectionUnavailableError {}
|
|
896
|
+
|
|
897
|
+
function* readProjectedThreadRoutingBatches(database, roomId, rootNumbers, identities) {
|
|
898
|
+
if (!rootNumbers.length || !identities.length) return new Map();
|
|
872
899
|
const keysByHash = new Map();
|
|
873
900
|
const durableKeysByHash = new Map();
|
|
874
901
|
for (const identity of identities) {
|
|
@@ -938,7 +965,7 @@ export async function getLocalThreadRoutingAgentKeysForRoots(
|
|
|
938
965
|
keys.add(agentKey);
|
|
939
966
|
result.set(root, keys);
|
|
940
967
|
}
|
|
941
|
-
|
|
968
|
+
yield;
|
|
942
969
|
}
|
|
943
970
|
|
|
944
971
|
const aliasInputs = [];
|
|
@@ -1032,7 +1059,7 @@ export async function getLocalThreadRoutingAgentKeysForRoots(
|
|
|
1032
1059
|
keys.add(agentKey);
|
|
1033
1060
|
result.set(root, keys);
|
|
1034
1061
|
}
|
|
1035
|
-
|
|
1062
|
+
yield;
|
|
1036
1063
|
}
|
|
1037
1064
|
return result;
|
|
1038
1065
|
}
|