@cogitator-ai/channels 0.15.12 → 0.16.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 +88 -23
- package/dist/assistant-config.d.ts +38 -0
- package/dist/assistant-config.d.ts.map +1 -1
- package/dist/assistant-config.js +61 -0
- package/dist/assistant-config.js.map +1 -1
- package/dist/channels/bluesky/channel.d.ts.map +1 -1
- package/dist/channels/bluesky/channel.js +1 -0
- package/dist/channels/bluesky/channel.js.map +1 -1
- package/dist/channels/github/channel.d.ts +222 -0
- package/dist/channels/github/channel.d.ts.map +1 -0
- package/dist/channels/github/channel.js +736 -0
- package/dist/channels/github/channel.js.map +1 -0
- package/dist/channels/github/client.d.ts +45 -0
- package/dist/channels/github/client.d.ts.map +1 -0
- package/dist/channels/github/client.js +119 -0
- package/dist/channels/github/client.js.map +1 -0
- package/dist/channels/github/events.d.ts +49 -0
- package/dist/channels/github/events.d.ts.map +1 -0
- package/dist/channels/github/events.js +147 -0
- package/dist/channels/github/events.js.map +1 -0
- package/dist/channels/github/index.d.ts +4 -0
- package/dist/channels/github/index.d.ts.map +1 -0
- package/dist/channels/github/index.js +4 -0
- package/dist/channels/github/index.js.map +1 -0
- package/dist/channels/slack.d.ts.map +1 -1
- package/dist/channels/slack.js +2 -1
- package/dist/channels/slack.js.map +1 -1
- package/dist/channels/telegram.d.ts.map +1 -1
- package/dist/channels/telegram.js +14 -6
- package/dist/channels/telegram.js.map +1 -1
- package/dist/channels/threads/channel.d.ts.map +1 -1
- package/dist/channels/threads/channel.js +1 -0
- package/dist/channels/threads/channel.js.map +1 -1
- package/dist/channels/webchat.d.ts +8 -0
- package/dist/channels/webchat.d.ts.map +1 -1
- package/dist/channels/webchat.js +54 -15
- package/dist/channels/webchat.js.map +1 -1
- package/dist/channels/whatsapp.d.ts.map +1 -1
- package/dist/channels/whatsapp.js +13 -0
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/formatters/markdown.d.ts.map +1 -1
- package/dist/formatters/markdown.js +1 -0
- package/dist/formatters/markdown.js.map +1 -1
- package/dist/gateway.d.ts +69 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +249 -40
- package/dist/gateway.js.map +1 -1
- package/dist/heartbeat.d.ts +5 -1
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +13 -3
- package/dist/heartbeat.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/merge-messages.d.ts +3 -2
- package/dist/merge-messages.d.ts.map +1 -1
- package/dist/merge-messages.js +4 -2
- package/dist/merge-messages.js.map +1 -1
- package/dist/message-queue.d.ts +14 -4
- package/dist/message-queue.d.ts.map +1 -1
- package/dist/message-queue.js +36 -35
- package/dist/message-queue.js.map +1 -1
- package/dist/runtime-builder.d.ts.map +1 -1
- package/dist/runtime-builder.js +39 -5
- package/dist/runtime-builder.js.map +1 -1
- package/dist/thread-key.d.ts +50 -0
- package/dist/thread-key.d.ts.map +1 -0
- package/dist/thread-key.js +82 -0
- package/dist/thread-key.js.map +1 -0
- package/package.json +8 -7
package/dist/gateway.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface GatewayFullConfig extends GatewayConfig {
|
|
|
23
23
|
* the prompt cache they open with
|
|
24
24
|
*/
|
|
25
25
|
brief?: (msg: ChannelMessage) => string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* How long {@link Gateway.stop} waits for the queued messages and the runs in progress to be
|
|
28
|
+
* answered, in milliseconds (default 30000). What is still running then is aborted, and what
|
|
29
|
+
* is still waiting is dropped.
|
|
30
|
+
*/
|
|
31
|
+
drainTimeout?: number;
|
|
26
32
|
}
|
|
27
33
|
export interface GatewaySessionInfo {
|
|
28
34
|
threadId: string;
|
|
@@ -33,6 +39,8 @@ export interface GatewaySessionInfo {
|
|
|
33
39
|
lastActiveAt: number;
|
|
34
40
|
active: boolean;
|
|
35
41
|
}
|
|
42
|
+
/** Threads kept in the gateway's own bookkeeping (stats, sessions, envelope times) */
|
|
43
|
+
export declare const MAX_TRACKED_THREADS = 10000;
|
|
36
44
|
export declare class Gateway {
|
|
37
45
|
private readonly config;
|
|
38
46
|
private readonly channels;
|
|
@@ -44,6 +52,8 @@ export declare class Gateway {
|
|
|
44
52
|
private readonly hooks;
|
|
45
53
|
private readonly approvalWords;
|
|
46
54
|
private readonly pausedThreads;
|
|
55
|
+
/** The stop of every run in progress */
|
|
56
|
+
private readonly runs;
|
|
47
57
|
/** The stop of each run, by the signal the run was given */
|
|
48
58
|
private readonly stops;
|
|
49
59
|
/** The stop of each run streaming into a draft with a stop button, by channel, chat and draft */
|
|
@@ -53,16 +63,45 @@ export declare class Gateway {
|
|
|
53
63
|
private readonly lastMessageTime;
|
|
54
64
|
private readonly threads;
|
|
55
65
|
private readonly inFlight;
|
|
66
|
+
/** Messages taken and not answered yet, for {@link idle} */
|
|
67
|
+
private readonly pending;
|
|
68
|
+
/** The stop in progress, which a second {@link stop} call returns */
|
|
69
|
+
private stopping;
|
|
70
|
+
/** The last pending write of each user's session, so writes to one session never interleave */
|
|
71
|
+
private readonly sessionWrites;
|
|
56
72
|
private running;
|
|
57
73
|
private startedAt;
|
|
58
74
|
private messageCount;
|
|
59
75
|
private messageCountDay;
|
|
60
76
|
constructor(config: GatewayFullConfig);
|
|
61
77
|
start(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Stops the gateway. Channels have already confirmed the messages they handed over, so it
|
|
80
|
+
* first lets them be answered: messages held by `debounce` are flushed, and it waits up to
|
|
81
|
+
* `drainTimeout` for the queues and the runs in progress. Then it aborts the runs still going,
|
|
82
|
+
* drops the messages still waiting, waits for the aborted runs to settle (up to `drainTimeout`
|
|
83
|
+
* again) and stops the channels. Messages that come in meanwhile are refused (see
|
|
84
|
+
* {@link injectMessage}). A second call while it stops returns the same promise.
|
|
85
|
+
*/
|
|
62
86
|
stop(): Promise<void>;
|
|
87
|
+
private drainAndStop;
|
|
63
88
|
get stats(): GatewayStats;
|
|
64
89
|
getSessions(): GatewaySessionInfo[];
|
|
90
|
+
/**
|
|
91
|
+
* Feeds a message to the gateway as if a channel had received it (the scheduler sends its
|
|
92
|
+
* tasks this way). It resolves once the message is answered, or once it is buffered when
|
|
93
|
+
* `debounce` is on. It rejects when the message was not answered: its run failed (also
|
|
94
|
+
* reported to `onError`), it was aborted or dropped by {@link stop}, or it came in while the
|
|
95
|
+
* gateway was stopping.
|
|
96
|
+
*/
|
|
65
97
|
injectMessage(msg: ChannelMessage): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Resolves once every message the gateway has taken is answered: those waiting in their
|
|
100
|
+
* thread's queue and the runs in progress, including messages that come in while it waits.
|
|
101
|
+
* Messages held by `debounce` count from the moment they are flushed. {@link stop} waits
|
|
102
|
+
* for it, up to `drainTimeout`.
|
|
103
|
+
*/
|
|
104
|
+
idle(): Promise<void>;
|
|
66
105
|
/**
|
|
67
106
|
* Summarize older messages of a conversation thread, keeping `keepRecent` messages intact.
|
|
68
107
|
* Runs regardless of the configured thresholds.
|
|
@@ -77,7 +116,8 @@ export declare class Gateway {
|
|
|
77
116
|
compactAll(): Promise<string>;
|
|
78
117
|
/**
|
|
79
118
|
* The threads {@link compactAll} goes through: those served since the gateway started, and the
|
|
80
|
-
*
|
|
119
|
+
* threads of each stored session when they are keyed by the default key (the conversations kept
|
|
120
|
+
* in the session's metadata, or the chat it was created in) or by `legacyThreadKey`
|
|
81
121
|
*/
|
|
82
122
|
private knownThreads;
|
|
83
123
|
/**
|
|
@@ -98,10 +138,28 @@ export declare class Gateway {
|
|
|
98
138
|
* decision, or go away when the request was dropped.
|
|
99
139
|
*/
|
|
100
140
|
private closePrompts;
|
|
141
|
+
/**
|
|
142
|
+
* A message from a channel, a button or {@link injectMessage}. With a queue, a channel's message
|
|
143
|
+
* is only queued, so the channel goes on to its next update while the thread waits its turn.
|
|
144
|
+
* `untilAnswered` waits for the answer.
|
|
145
|
+
*/
|
|
101
146
|
private handleIncoming;
|
|
147
|
+
/** Puts a message through the queue of its thread, resolving once it is done with */
|
|
102
148
|
private dispatch;
|
|
149
|
+
/** Resolves `true` once the message is handled, `false` when it failed or was dropped */
|
|
150
|
+
private route;
|
|
151
|
+
/**
|
|
152
|
+
* A scheduled message names only its chat, so with the default thread key it gets the topic or
|
|
153
|
+
* conversation of the user's most recent conversation in that chat, kept in their session: the
|
|
154
|
+
* task is then answered in the thread and topic it was set up in.
|
|
155
|
+
*/
|
|
156
|
+
private locateScheduled;
|
|
103
157
|
private rollMessageCounter;
|
|
104
158
|
private runMiddleware;
|
|
159
|
+
/**
|
|
160
|
+
* Handles one message, `false` when its run failed or was aborted. A message for a channel
|
|
161
|
+
* the gateway does not run counts as handled: it never can be.
|
|
162
|
+
*/
|
|
105
163
|
private processMessage;
|
|
106
164
|
private processWithChannel;
|
|
107
165
|
private handleAccepted;
|
|
@@ -128,6 +186,16 @@ export declare class Gateway {
|
|
|
128
186
|
*/
|
|
129
187
|
private streamRun;
|
|
130
188
|
private getThreadId;
|
|
189
|
+
/**
|
|
190
|
+
* Keeps the message's conversation in the user's session, so its thread is known after a
|
|
191
|
+
* restart. A failure is reported to `onError` and does not keep the message from its answer.
|
|
192
|
+
*/
|
|
193
|
+
private rememberConversation;
|
|
194
|
+
/**
|
|
195
|
+
* Runs `write` after every earlier write to the same session has finished, so a read, change
|
|
196
|
+
* and write of its metadata never loses another one's change.
|
|
197
|
+
*/
|
|
198
|
+
private writeSession;
|
|
131
199
|
private resolveAgent;
|
|
132
200
|
private createMiddlewareContext;
|
|
133
201
|
/**
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EAIZ,cAAc,EAMd,YAAY,
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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"}
|
package/dist/gateway.js
CHANGED
|
@@ -7,6 +7,7 @@ import { StatusReactionTracker } from './status-reactions.js';
|
|
|
7
7
|
import { InboundDebouncer } from './inbound-debounce.js';
|
|
8
8
|
import { formatEnvelope } from './envelope.js';
|
|
9
9
|
import { MessageQueue } from './message-queue.js';
|
|
10
|
+
import { CONVERSATIONS_KEY, defaultThreadKey, findConversation, legacyThreadKey, readConversations, rememberConversation, } from './thread-key.js';
|
|
10
11
|
import { DEFAULT_APPROVE_WORDS, DEFAULT_DENY_WORDS, DEFAULT_NOT_ALLOWED_MESSAGE, DEFAULT_EXPIRED_MESSAGE, APPROVE_ACTION, DENY_ACTION, formatApprovalPrompt, parseApprovalReply, } from './approvals.js';
|
|
11
12
|
/** Markdown as the channel takes it: as written when it renders Markdown itself. */
|
|
12
13
|
function adaptFor(channel, msg, text) {
|
|
@@ -26,6 +27,34 @@ const DEFAULT_SUMMARY_PROMPT = 'Summarize the following conversation between a u
|
|
|
26
27
|
'Preserve facts, decisions, user preferences, open tasks and any commitments. ' +
|
|
27
28
|
'Write a concise summary in the language of the conversation.';
|
|
28
29
|
const TYPING_INTERVAL_MS = 4000;
|
|
30
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 30_000;
|
|
31
|
+
/** Why {@link Gateway.stop} aborts the runs still going past `drainTimeout` */
|
|
32
|
+
const GATEWAY_STOPPED = new Error('The gateway stopped');
|
|
33
|
+
/** Threads kept in the gateway's own bookkeeping (stats, sessions, envelope times) */
|
|
34
|
+
export const MAX_TRACKED_THREADS = 10_000;
|
|
35
|
+
/** Sets `key` as the most recent entry of `map`, dropping the least recent one past the limit */
|
|
36
|
+
function touch(map, key, value) {
|
|
37
|
+
map.delete(key);
|
|
38
|
+
map.set(key, value);
|
|
39
|
+
if (map.size > MAX_TRACKED_THREADS) {
|
|
40
|
+
const oldest = map.keys().next();
|
|
41
|
+
if (!oldest.done)
|
|
42
|
+
map.delete(oldest.value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Resolves `true` once `promise` settles, or `false` after `ms` */
|
|
46
|
+
async function settlesWithin(promise, ms) {
|
|
47
|
+
let timer;
|
|
48
|
+
const timeout = new Promise((resolve) => {
|
|
49
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
return await Promise.race([promise.then(() => true), timeout]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
29
58
|
function messageText(message) {
|
|
30
59
|
if (typeof message.content === 'string')
|
|
31
60
|
return message.content;
|
|
@@ -33,10 +62,6 @@ function messageText(message) {
|
|
|
33
62
|
.map((part) => (part.type === 'text' ? part.text : `[${part.type}]`))
|
|
34
63
|
.join(' ');
|
|
35
64
|
}
|
|
36
|
-
/** The thread of a user on a channel when `session.threadKey` does not name it */
|
|
37
|
-
function defaultThreadId(channelType, userId) {
|
|
38
|
-
return `${channelType}:${userId}`;
|
|
39
|
-
}
|
|
40
65
|
function isScheduled(msg) {
|
|
41
66
|
const raw = msg.raw;
|
|
42
67
|
return typeof raw === 'object' && raw !== null && 'scheduled' in raw && raw.scheduled === true;
|
|
@@ -63,6 +88,8 @@ export class Gateway {
|
|
|
63
88
|
hooks;
|
|
64
89
|
approvalWords;
|
|
65
90
|
pausedThreads = new Map();
|
|
91
|
+
/** The stop of every run in progress */
|
|
92
|
+
runs = new Set();
|
|
66
93
|
/** The stop of each run, by the signal the run was given */
|
|
67
94
|
stops = new WeakMap();
|
|
68
95
|
/** The stop of each run streaming into a draft with a stop button, by channel, chat and draft */
|
|
@@ -72,6 +99,12 @@ export class Gateway {
|
|
|
72
99
|
lastMessageTime = new Map();
|
|
73
100
|
threads = new Map();
|
|
74
101
|
inFlight = new Map();
|
|
102
|
+
/** Messages taken and not answered yet, for {@link idle} */
|
|
103
|
+
pending = new Set();
|
|
104
|
+
/** The stop in progress, which a second {@link stop} call returns */
|
|
105
|
+
stopping = null;
|
|
106
|
+
/** The last pending write of each user's session, so writes to one session never interleave */
|
|
107
|
+
sessionWrites = new Map();
|
|
75
108
|
running = false;
|
|
76
109
|
startedAt = null;
|
|
77
110
|
messageCount = 0;
|
|
@@ -88,15 +121,18 @@ export class Gateway {
|
|
|
88
121
|
};
|
|
89
122
|
this.sessionManager =
|
|
90
123
|
config.sessionManager ?? (config.memory ? new SessionManager(config.memory) : null);
|
|
124
|
+
const queueMode = config.queueMode ?? 'sequential';
|
|
91
125
|
this.messageQueue =
|
|
92
|
-
|
|
93
|
-
?
|
|
94
|
-
:
|
|
126
|
+
queueMode === 'parallel'
|
|
127
|
+
? null
|
|
128
|
+
: new MessageQueue(queueMode, (msg, signal) => this.processMessage(msg, signal));
|
|
95
129
|
this.debouncer = config.debounce?.enabled
|
|
96
|
-
? new InboundDebouncer(config.debounce, (merged) =>
|
|
130
|
+
? new InboundDebouncer(config.debounce, async (merged) => {
|
|
131
|
+
this.dispatch(merged).catch(() => { });
|
|
132
|
+
})
|
|
97
133
|
: null;
|
|
98
134
|
for (const channel of this.channels) {
|
|
99
|
-
channel.onMessage((msg) => this.handleIncoming(msg));
|
|
135
|
+
channel.onMessage((msg) => this.handleIncoming(msg, false));
|
|
100
136
|
channel.onAction?.((action) => this.handleAction(channel, action));
|
|
101
137
|
channel.onStop?.((stop) => {
|
|
102
138
|
this.stoppableDrafts
|
|
@@ -119,12 +155,38 @@ export class Gateway {
|
|
|
119
155
|
throw failure.reason instanceof Error ? failure.reason : new Error(String(failure.reason));
|
|
120
156
|
}
|
|
121
157
|
}
|
|
122
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Stops the gateway. Channels have already confirmed the messages they handed over, so it
|
|
160
|
+
* first lets them be answered: messages held by `debounce` are flushed, and it waits up to
|
|
161
|
+
* `drainTimeout` for the queues and the runs in progress. Then it aborts the runs still going,
|
|
162
|
+
* drops the messages still waiting, waits for the aborted runs to settle (up to `drainTimeout`
|
|
163
|
+
* again) and stops the channels. Messages that come in meanwhile are refused (see
|
|
164
|
+
* {@link injectMessage}). A second call while it stops returns the same promise.
|
|
165
|
+
*/
|
|
166
|
+
stop() {
|
|
167
|
+
if (this.stopping)
|
|
168
|
+
return this.stopping;
|
|
123
169
|
if (!this.running)
|
|
124
|
-
return;
|
|
170
|
+
return Promise.resolve();
|
|
125
171
|
this.running = false;
|
|
126
|
-
|
|
172
|
+
const stopping = this.drainAndStop().finally(() => {
|
|
173
|
+
this.stopping = null;
|
|
174
|
+
});
|
|
175
|
+
this.stopping = stopping;
|
|
176
|
+
return stopping;
|
|
177
|
+
}
|
|
178
|
+
async drainAndStop() {
|
|
179
|
+
const timeout = this.config.drainTimeout ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
180
|
+
if (this.debouncer) {
|
|
127
181
|
await this.debouncer.flushAll();
|
|
182
|
+
this.debouncer.dispose();
|
|
183
|
+
}
|
|
184
|
+
if (!(await settlesWithin(this.idle(), timeout))) {
|
|
185
|
+
this.messageQueue?.dispose();
|
|
186
|
+
for (const run of this.runs)
|
|
187
|
+
run.abort(GATEWAY_STOPPED);
|
|
188
|
+
await settlesWithin(this.idle(), timeout);
|
|
189
|
+
}
|
|
128
190
|
this.messageQueue?.dispose();
|
|
129
191
|
await Promise.all(this.channels.map((ch) => ch.stop()));
|
|
130
192
|
this.startedAt = null;
|
|
@@ -144,8 +206,26 @@ export class Gateway {
|
|
|
144
206
|
.map(([threadId, info]) => ({ threadId, ...info, active: this.inFlight.has(threadId) }))
|
|
145
207
|
.sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
146
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Feeds a message to the gateway as if a channel had received it (the scheduler sends its
|
|
211
|
+
* tasks this way). It resolves once the message is answered, or once it is buffered when
|
|
212
|
+
* `debounce` is on. It rejects when the message was not answered: its run failed (also
|
|
213
|
+
* reported to `onError`), it was aborted or dropped by {@link stop}, or it came in while the
|
|
214
|
+
* gateway was stopping.
|
|
215
|
+
*/
|
|
147
216
|
async injectMessage(msg) {
|
|
148
|
-
return this.handleIncoming(msg);
|
|
217
|
+
return this.handleIncoming(msg, true);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolves once every message the gateway has taken is answered: those waiting in their
|
|
221
|
+
* thread's queue and the runs in progress, including messages that come in while it waits.
|
|
222
|
+
* Messages held by `debounce` count from the moment they are flushed. {@link stop} waits
|
|
223
|
+
* for it, up to `drainTimeout`.
|
|
224
|
+
*/
|
|
225
|
+
async idle() {
|
|
226
|
+
while (this.pending.size > 0) {
|
|
227
|
+
await Promise.allSettled(this.pending);
|
|
228
|
+
}
|
|
149
229
|
}
|
|
150
230
|
/**
|
|
151
231
|
* Summarize older messages of a conversation thread, keeping `keepRecent` messages intact.
|
|
@@ -196,16 +276,31 @@ export class Gateway {
|
|
|
196
276
|
}
|
|
197
277
|
/**
|
|
198
278
|
* The threads {@link compactAll} goes through: those served since the gateway started, and the
|
|
199
|
-
*
|
|
279
|
+
* threads of each stored session when they are keyed by the default key (the conversations kept
|
|
280
|
+
* in the session's metadata, or the chat it was created in) or by `legacyThreadKey`
|
|
200
281
|
*/
|
|
201
282
|
async knownThreads() {
|
|
202
283
|
const threadIds = new Set(this.threads.keys());
|
|
203
|
-
|
|
284
|
+
const threadKey = this.config.session?.threadKey;
|
|
285
|
+
if (!this.sessionManager || (threadKey && threadKey !== legacyThreadKey)) {
|
|
204
286
|
return { threadIds: [...threadIds] };
|
|
205
287
|
}
|
|
206
288
|
try {
|
|
207
289
|
for (const session of await this.sessionManager.list()) {
|
|
208
|
-
|
|
290
|
+
if (threadKey) {
|
|
291
|
+
threadIds.add(legacyThreadKey(session));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const conversations = readConversations(session.metadata);
|
|
295
|
+
for (const conversation of conversations.length > 0
|
|
296
|
+
? conversations
|
|
297
|
+
: [{ channelId: session.channelId }]) {
|
|
298
|
+
threadIds.add(defaultThreadKey({
|
|
299
|
+
channelType: session.channelType,
|
|
300
|
+
userId: session.userId,
|
|
301
|
+
...conversation,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
209
304
|
}
|
|
210
305
|
}
|
|
211
306
|
catch (error) {
|
|
@@ -272,6 +367,7 @@ export class Gateway {
|
|
|
272
367
|
...(action.userName ? { userName: action.userName } : {}),
|
|
273
368
|
...(action.channelId.startsWith('-') ? { groupId: action.channelId } : {}),
|
|
274
369
|
...(action.topicId ? { topicId: action.topicId } : {}),
|
|
370
|
+
...(action.conversationId ? { conversationId: action.conversationId } : {}),
|
|
275
371
|
text: (approve ? this.approvalWords.approveWords : this.approvalWords.denyWords)[0] ?? '',
|
|
276
372
|
raw: action.raw,
|
|
277
373
|
};
|
|
@@ -300,7 +396,7 @@ export class Gateway {
|
|
|
300
396
|
}
|
|
301
397
|
await answer();
|
|
302
398
|
await this.closePrompts(prompt.threadId, approve);
|
|
303
|
-
await this.handleIncoming(msg);
|
|
399
|
+
await this.handleIncoming(msg, false);
|
|
304
400
|
}
|
|
305
401
|
/**
|
|
306
402
|
* Retires the buttons of a thread's approval prompts once it is answered: they show the
|
|
@@ -330,19 +426,83 @@ export class Gateway {
|
|
|
330
426
|
.catch(() => { });
|
|
331
427
|
}
|
|
332
428
|
}
|
|
333
|
-
|
|
429
|
+
/**
|
|
430
|
+
* A message from a channel, a button or {@link injectMessage}. With a queue, a channel's message
|
|
431
|
+
* is only queued, so the channel goes on to its next update while the thread waits its turn.
|
|
432
|
+
* `untilAnswered` waits for the answer.
|
|
433
|
+
*/
|
|
434
|
+
async handleIncoming(msg, untilAnswered) {
|
|
435
|
+
if (this.stopping) {
|
|
436
|
+
const refused = new Error('The gateway is stopping: the message was dropped');
|
|
437
|
+
this.config.onError?.(refused, msg);
|
|
438
|
+
if (untilAnswered)
|
|
439
|
+
throw refused;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
334
442
|
if (this.debouncer) {
|
|
335
443
|
this.debouncer.enqueue(msg);
|
|
336
444
|
return;
|
|
337
445
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
446
|
+
const answered = this.dispatch(msg);
|
|
447
|
+
if (untilAnswered || !this.messageQueue) {
|
|
448
|
+
if (!(await answered) && untilAnswered) {
|
|
449
|
+
throw new Error('The message was not answered: its run failed or was aborted');
|
|
450
|
+
}
|
|
343
451
|
return;
|
|
344
452
|
}
|
|
345
|
-
|
|
453
|
+
answered.catch(() => { });
|
|
454
|
+
}
|
|
455
|
+
/** Puts a message through the queue of its thread, resolving once it is done with */
|
|
456
|
+
dispatch(msg) {
|
|
457
|
+
const answered = this.route(msg);
|
|
458
|
+
this.pending.add(answered);
|
|
459
|
+
const forget = () => {
|
|
460
|
+
this.pending.delete(answered);
|
|
461
|
+
};
|
|
462
|
+
answered.then(forget, forget);
|
|
463
|
+
return answered;
|
|
464
|
+
}
|
|
465
|
+
/** Resolves `true` once the message is handled, `false` when it failed or was dropped */
|
|
466
|
+
async route(incoming) {
|
|
467
|
+
const msg = await this.locateScheduled(incoming);
|
|
468
|
+
if (this.messageQueue)
|
|
469
|
+
return this.messageQueue.push(msg, this.getThreadId(msg));
|
|
470
|
+
return this.processMessage(msg);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* A scheduled message names only its chat, so with the default thread key it gets the topic or
|
|
474
|
+
* conversation of the user's most recent conversation in that chat, kept in their session: the
|
|
475
|
+
* task is then answered in the thread and topic it was set up in.
|
|
476
|
+
*/
|
|
477
|
+
async locateScheduled(msg) {
|
|
478
|
+
if (!isScheduled(msg) ||
|
|
479
|
+
msg.topicId ||
|
|
480
|
+
msg.conversationId ||
|
|
481
|
+
!this.sessionManager ||
|
|
482
|
+
this.config.session?.threadKey) {
|
|
483
|
+
return msg;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const [session] = await this.sessionManager.list({
|
|
487
|
+
userId: msg.userId,
|
|
488
|
+
channelType: msg.channelType,
|
|
489
|
+
limit: 1,
|
|
490
|
+
});
|
|
491
|
+
const conversation = session
|
|
492
|
+
? findConversation(readConversations(session.metadata), msg.channelId)
|
|
493
|
+
: undefined;
|
|
494
|
+
if (!conversation)
|
|
495
|
+
return msg;
|
|
496
|
+
return {
|
|
497
|
+
...msg,
|
|
498
|
+
...(conversation.topicId && { topicId: conversation.topicId }),
|
|
499
|
+
...(conversation.conversationId && { conversationId: conversation.conversationId }),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
this.config.onError?.(toError(error), msg);
|
|
504
|
+
return msg;
|
|
505
|
+
}
|
|
346
506
|
}
|
|
347
507
|
rollMessageCounter() {
|
|
348
508
|
const today = dayKey(Date.now());
|
|
@@ -363,24 +523,31 @@ export class Gateway {
|
|
|
363
523
|
await runAll(0);
|
|
364
524
|
return reachedEnd;
|
|
365
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Handles one message, `false` when its run failed or was aborted. A message for a channel
|
|
528
|
+
* the gateway does not run counts as handled: it never can be.
|
|
529
|
+
*/
|
|
366
530
|
async processMessage(msg, signal) {
|
|
367
531
|
const channel = this.channels.find((ch) => ch.type === msg.channelType);
|
|
368
532
|
if (!channel)
|
|
369
|
-
return;
|
|
533
|
+
return true;
|
|
370
534
|
const threadId = this.getThreadId(msg);
|
|
371
535
|
this.inFlight.set(threadId, (this.inFlight.get(threadId) ?? 0) + 1);
|
|
372
536
|
const stop = new AbortController();
|
|
373
537
|
const runSignal = signal ? AbortSignal.any([signal, stop.signal]) : stop.signal;
|
|
374
538
|
this.stops.set(runSignal, stop);
|
|
539
|
+
this.runs.add(stop);
|
|
375
540
|
try {
|
|
376
541
|
await this.processWithChannel(msg, channel, threadId, runSignal);
|
|
542
|
+
return signal?.aborted !== true && stop.signal.reason !== GATEWAY_STOPPED;
|
|
377
543
|
}
|
|
378
544
|
catch (error) {
|
|
379
|
-
if (runSignal.aborted)
|
|
380
|
-
|
|
381
|
-
|
|
545
|
+
if (!runSignal.aborted)
|
|
546
|
+
this.config.onError?.(toError(error), msg);
|
|
547
|
+
return false;
|
|
382
548
|
}
|
|
383
549
|
finally {
|
|
550
|
+
this.runs.delete(stop);
|
|
384
551
|
const remaining = (this.inFlight.get(threadId) ?? 1) - 1;
|
|
385
552
|
if (remaining <= 0)
|
|
386
553
|
this.inFlight.delete(threadId);
|
|
@@ -404,7 +571,7 @@ export class Gateway {
|
|
|
404
571
|
this.rollMessageCounter();
|
|
405
572
|
this.messageCount++;
|
|
406
573
|
const known = this.threads.get(threadId);
|
|
407
|
-
this.threads
|
|
574
|
+
touch(this.threads, threadId, {
|
|
408
575
|
channelType: msg.channelType,
|
|
409
576
|
userId: msg.userId,
|
|
410
577
|
...(msg.userName || known?.userName ? { userName: msg.userName ?? known?.userName } : {}),
|
|
@@ -413,16 +580,22 @@ export class Gateway {
|
|
|
413
580
|
});
|
|
414
581
|
const agent = await this.resolveAgent(user);
|
|
415
582
|
if (this.sessionManager) {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
583
|
+
const sessionManager = this.sessionManager;
|
|
584
|
+
await this.writeSession(`${msg.channelType}:${msg.userId}`, async () => {
|
|
585
|
+
const session = await sessionManager.getOrCreate({
|
|
586
|
+
userId: msg.userId,
|
|
587
|
+
channelType: msg.channelType,
|
|
588
|
+
channelId: msg.channelId,
|
|
589
|
+
agentId: agent.name,
|
|
590
|
+
});
|
|
591
|
+
if (session.messageCount === 0) {
|
|
592
|
+
await this.hooks?.emit('session:created', { session, threadId });
|
|
593
|
+
}
|
|
594
|
+
if (!this.config.session?.threadKey && !isScheduled(msg)) {
|
|
595
|
+
await this.rememberConversation(sessionManager, session, msg);
|
|
596
|
+
}
|
|
597
|
+
await sessionManager.incrementMessageCount?.(session.id);
|
|
421
598
|
});
|
|
422
|
-
if (session.messageCount === 0) {
|
|
423
|
-
await this.hooks?.emit('session:created', { session, threadId });
|
|
424
|
-
}
|
|
425
|
-
await this.sessionManager.incrementMessageCount?.(session.id);
|
|
426
599
|
}
|
|
427
600
|
const decision = this.readApprovalReply(msg, threadId, known !== undefined);
|
|
428
601
|
if (!decision && this.config.memory && this.config.session?.compaction) {
|
|
@@ -480,7 +653,7 @@ export class Gateway {
|
|
|
480
653
|
if (this.config.envelope?.enabled) {
|
|
481
654
|
const prevTime = this.lastMessageTime.get(threadId);
|
|
482
655
|
msg = { ...msg, text: formatEnvelope(msg, this.config.envelope, prevTime) };
|
|
483
|
-
this.lastMessageTime
|
|
656
|
+
touch(this.lastMessageTime, threadId, Date.now());
|
|
484
657
|
}
|
|
485
658
|
const { input, images } = await this.extractMedia(msg, agent);
|
|
486
659
|
const options = {
|
|
@@ -619,6 +792,8 @@ export class Gateway {
|
|
|
619
792
|
userId: msg.userId,
|
|
620
793
|
channelType: msg.channelType,
|
|
621
794
|
channelId: msg.channelId,
|
|
795
|
+
...(msg.topicId && { topicId: msg.topicId }),
|
|
796
|
+
...(msg.conversationId && { conversationId: msg.conversationId }),
|
|
622
797
|
...(signal ? { signal } : {}),
|
|
623
798
|
...(this.config.runTimeout !== undefined ? { timeout: this.config.runTimeout } : {}),
|
|
624
799
|
...(tracker
|
|
@@ -774,7 +949,41 @@ export class Gateway {
|
|
|
774
949
|
if (this.config.session?.threadKey) {
|
|
775
950
|
return this.config.session.threadKey(msg);
|
|
776
951
|
}
|
|
777
|
-
return
|
|
952
|
+
return defaultThreadKey(msg);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Keeps the message's conversation in the user's session, so its thread is known after a
|
|
956
|
+
* restart. A failure is reported to `onError` and does not keep the message from its answer.
|
|
957
|
+
*/
|
|
958
|
+
async rememberConversation(sessionManager, session, msg) {
|
|
959
|
+
const conversations = rememberConversation(readConversations(session.metadata), msg);
|
|
960
|
+
if (!conversations)
|
|
961
|
+
return;
|
|
962
|
+
try {
|
|
963
|
+
await sessionManager.update(session.id, {
|
|
964
|
+
metadata: { [CONVERSATIONS_KEY]: conversations },
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
this.config.onError?.(toError(error), msg);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Runs `write` after every earlier write to the same session has finished, so a read, change
|
|
973
|
+
* and write of its metadata never loses another one's change.
|
|
974
|
+
*/
|
|
975
|
+
async writeSession(key, write) {
|
|
976
|
+
const previous = this.sessionWrites.get(key) ?? Promise.resolve();
|
|
977
|
+
const current = previous.then(write);
|
|
978
|
+
const settled = current.catch(() => { });
|
|
979
|
+
this.sessionWrites.set(key, settled);
|
|
980
|
+
try {
|
|
981
|
+
await current;
|
|
982
|
+
}
|
|
983
|
+
finally {
|
|
984
|
+
if (this.sessionWrites.get(key) === settled)
|
|
985
|
+
this.sessionWrites.delete(key);
|
|
986
|
+
}
|
|
778
987
|
}
|
|
779
988
|
async resolveAgent(user) {
|
|
780
989
|
const agentOrFactory = this.config.agent;
|