@grknbyk/agent-wire 0.6.0 → 0.6.1
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 -1
- package/package.json +1 -1
- package/src/mcp.mjs +15 -7
- package/src/slack.mjs +36 -10
package/README.md
CHANGED
package/package.json
CHANGED
package/src/mcp.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path';
|
|
|
10
10
|
import { activeChannels, channelMode, findChannel, loadConfig, paths, pollableChannels } from './config.mjs';
|
|
11
11
|
import { DEFAULT_COUNT, appendMessages, archive, findByTs, markRead, readCursor, selectMessages, writeCursor } from './inbox.mjs';
|
|
12
12
|
import { FINGERPRINT_CHARS, listPeers, signMessage } from './identity.mjs';
|
|
13
|
-
import { listMembers, pollChannel, postMessage, slackClient, uploadFile } from './slack.mjs';
|
|
13
|
+
import { CHANNEL_CONCURRENCY, listMembers, mapLimit, pollChannel, postMessage, slackClient, uploadFile } from './slack.mjs';
|
|
14
14
|
import { MAX_HOPS, TEXT_MAX, formatMessage, mintNonce, renderEnvelope } from './protocol.mjs';
|
|
15
15
|
|
|
16
16
|
const POLL_EVERY_MS = 5000;
|
|
@@ -134,18 +134,26 @@ function claimsPoll() {
|
|
|
134
134
|
return true;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// The channels are fetched together and written afterwards, in order. Awaiting
|
|
138
|
+
// one channel before starting the next spent a round trip per channel on data
|
|
139
|
+
// that has nothing to do with the previous answer. Writing afterwards also means
|
|
140
|
+
// no two channels interleave a read-modify-write of the same log.
|
|
141
|
+
//
|
|
142
|
+
// A channel that throws is caught here rather than at the caller, so one broken
|
|
143
|
+
// channel costs its own messages instead of everybody else's.
|
|
137
144
|
export async function pollOnce(config) {
|
|
138
145
|
const client = slackClient(config.bot_token);
|
|
146
|
+
const channels = pollableChannels(config);
|
|
147
|
+
const polled = await mapLimit(channels, CHANNEL_CONCURRENCY, (channel) =>
|
|
148
|
+
pollChannel(client, channel, { since: readCursor(channel.id), myNickname: config.nickname })
|
|
149
|
+
.catch((error) => ({ ok: false, reason: error.message, items: [] })));
|
|
150
|
+
|
|
139
151
|
let added = 0;
|
|
140
|
-
for (const
|
|
141
|
-
const result = await pollChannel(client, channel, {
|
|
142
|
-
since: readCursor(channel.id),
|
|
143
|
-
myNickname: config.nickname,
|
|
144
|
-
});
|
|
152
|
+
for (const [index, result] of polled.entries()) {
|
|
145
153
|
if (!result.ok) continue;
|
|
146
154
|
|
|
147
155
|
added += appendMessages(result.items);
|
|
148
|
-
if (result.newest) writeCursor(
|
|
156
|
+
if (result.newest) writeCursor(channels[index].id, result.newest);
|
|
149
157
|
}
|
|
150
158
|
return added;
|
|
151
159
|
}
|
package/src/slack.mjs
CHANGED
|
@@ -24,6 +24,34 @@ const NAME_MAX_CHARS = 80;
|
|
|
24
24
|
// message says why it was left there.
|
|
25
25
|
const DOWNLOAD_MAX_BYTES = 20 * 1024 * 1024;
|
|
26
26
|
|
|
27
|
+
// How many requests of one kind may be in flight. Slack rate-limits per method,
|
|
28
|
+
// so these are per loop rather than one global number: history is Tier 3, the
|
|
29
|
+
// user lookup is Tier 4, and a download is not an API call at all but does hold
|
|
30
|
+
// a whole file in memory while it lands.
|
|
31
|
+
export const CHANNEL_CONCURRENCY = 4;
|
|
32
|
+
const USER_CONCURRENCY = 8;
|
|
33
|
+
const DOWNLOAD_CONCURRENCY = 3;
|
|
34
|
+
|
|
35
|
+
// Node has no bounded Promise.all, and both ends of the choice are wrong here:
|
|
36
|
+
// awaiting in a loop costs one round trip per item, and an unbounded Promise.all
|
|
37
|
+
// throws two hundred requests at a rate limiter. The workers pull from one shared
|
|
38
|
+
// cursor rather than taking a slice each, so a slow reply cannot leave the others
|
|
39
|
+
// queued behind it.
|
|
40
|
+
export async function mapLimit(items, limit, run) {
|
|
41
|
+
const results = new Array(items.length);
|
|
42
|
+
let next = 0;
|
|
43
|
+
|
|
44
|
+
const worker = async () => {
|
|
45
|
+
while (next < items.length) {
|
|
46
|
+
const index = next++;
|
|
47
|
+
results[index] = await run(items[index], index);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
// conversations.* reject a JSON body and chat.postMessage needs one for metadata,
|
|
28
56
|
// so the client speaks both and the caller picks per method. The token rides along
|
|
29
57
|
// because downloading a file is a plain fetch, not an API call.
|
|
@@ -165,12 +193,8 @@ async function downloadAttachment(client, file) {
|
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
async function downloadAll(client, files) {
|
|
168
|
-
const saved = [];
|
|
169
|
-
|
|
170
|
-
const result = await downloadAttachment(client, file);
|
|
171
|
-
if (result) saved.push(result);
|
|
172
|
-
}
|
|
173
|
-
return saved;
|
|
196
|
+
const saved = await mapLimit(files ?? [], DOWNLOAD_CONCURRENCY, (file) => downloadAttachment(client, file));
|
|
197
|
+
return saved.filter(Boolean);
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
async function downloadById(client, fileId) {
|
|
@@ -191,13 +215,15 @@ async function resolveUserNames(client, userIds) {
|
|
|
191
215
|
const missing = [...new Set(userIds)].filter((userId) => !known[userId]);
|
|
192
216
|
if (missing.length === 0) return userIds.map((userId) => known[userId]);
|
|
193
217
|
|
|
194
|
-
const
|
|
195
|
-
for (const userId of missing) {
|
|
218
|
+
const resolved = await mapLimit(missing, USER_CONCURRENCY, async (userId) => {
|
|
196
219
|
const result = await client.form('users.info', { user: userId });
|
|
197
|
-
|
|
220
|
+
return result.ok
|
|
198
221
|
? (result.user.profile?.display_name || result.user.real_name || userId)
|
|
199
222
|
: userId;
|
|
200
|
-
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const found = { ...known };
|
|
226
|
+
missing.forEach((userId, index) => { found[userId] = resolved[index]; });
|
|
201
227
|
writeJson(paths.users, found);
|
|
202
228
|
return userIds.map((userId) => found[userId]);
|
|
203
229
|
}
|