@stratusagent/channel-slack 0.12.5 → 0.12.6
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 +27 -24
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -121
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -27,6 +27,17 @@ const THREAD_ADDRESSEE_CAPACITY = 2000;
|
|
|
27
27
|
* same message, and the addressee record answers for every message after.
|
|
28
28
|
*/
|
|
29
29
|
const COLD_VERDICT_CAPACITY = 256;
|
|
30
|
+
/**
|
|
31
|
+
* How many of one person's mentions one thread remembers (card 541). Only
|
|
32
|
+
* has to span the lag between two sockets, like `THREAD_HANDOVER_DEPTH`.
|
|
33
|
+
*/
|
|
34
|
+
const SENDER_ADDRESSEE_DEPTH = 8;
|
|
35
|
+
/**
|
|
36
|
+
* How many people one thread remembers an addressee for. Past this the
|
|
37
|
+
* least recently heard is forgotten, and their next untagged reply goes to
|
|
38
|
+
* the thread's owner, as after a restart.
|
|
39
|
+
*/
|
|
40
|
+
const THREAD_SENDER_CAPACITY = 64;
|
|
30
41
|
/**
|
|
31
42
|
* How many claims on one thread it remembers.
|
|
32
43
|
*
|
|
@@ -69,17 +80,6 @@ const toolStatus = (toolName) => `is running ${toolName}…`;
|
|
|
69
80
|
* approval wait — is exactly the one whose status must not vanish.
|
|
70
81
|
*/
|
|
71
82
|
const STATUS_REFRESH_MS = 60_000;
|
|
72
|
-
/**
|
|
73
|
-
* How long an agent that judges stays attentive after it last answered a
|
|
74
|
-
* message that addressed it: this many messages, or this many minutes,
|
|
75
|
-
* whichever ends first. Judging costs
|
|
76
|
-
* a model call per message, and two people talking for an hour is a
|
|
77
|
-
* hundred of them for one "no" — so attention decays the way a person's
|
|
78
|
-
* does. Outside the window a message is overheard for free, and a mention
|
|
79
|
-
* re-arms it, which is what a mention has always meant.
|
|
80
|
-
*/
|
|
81
|
-
const ATTENTION_MESSAGES = 8;
|
|
82
|
-
const ATTENTION_MS = 15 * 60 * 1000;
|
|
83
83
|
/**
|
|
84
84
|
* A 5 MB image over a slow link is seconds, not minutes. Past this the
|
|
85
85
|
* downloads are abandoned and the turn runs with the attachments named as
|
|
@@ -2293,6 +2293,67 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
2293
2293
|
* the addressee map; a lost entry costs a lookup.
|
|
2294
2294
|
*/
|
|
2295
2295
|
const threadOrigins = new Map();
|
|
2296
|
+
/**
|
|
2297
|
+
* Per thread, the agent each person last addressed by name, and when
|
|
2298
|
+
* (card 541). An untagged reply from that person is that agent's —
|
|
2299
|
+
* `@Bea your turn`, then `and?`, is Bea's — ahead of the thread's owner,
|
|
2300
|
+
* who keeps every reply from someone who has not addressed anyone else.
|
|
2301
|
+
* Recorded by every socket from the message itself, before any refusal,
|
|
2302
|
+
* as a handover is, so the sockets agree. Bounded like the addressee map;
|
|
2303
|
+
* a lost entry, or a restart, falls back to the owner.
|
|
2304
|
+
*/
|
|
2305
|
+
const senderAddressees = new Map();
|
|
2306
|
+
const rememberSenderAddressee = (key, sender, agentId, ts) => {
|
|
2307
|
+
const senders = senderAddressees.get(key) ?? new Map();
|
|
2308
|
+
// Kept in timestamp order, so a socket that is behind can still find the
|
|
2309
|
+
// mention that preceded the reply it is only now reaching. A late
|
|
2310
|
+
// delivery of an older mention slots in where it belongs.
|
|
2311
|
+
const history = (senders.get(sender) ?? []).filter((entry) => entry.ts !== ts);
|
|
2312
|
+
let at = history.length;
|
|
2313
|
+
while (at > 0 && history[at - 1].ts > ts) {
|
|
2314
|
+
at -= 1;
|
|
2315
|
+
}
|
|
2316
|
+
history.splice(at, 0, { agentId, ts });
|
|
2317
|
+
if (history.length > SENDER_ADDRESSEE_DEPTH) {
|
|
2318
|
+
history.splice(0, history.length - SENDER_ADDRESSEE_DEPTH);
|
|
2319
|
+
}
|
|
2320
|
+
// Most recently heard sender last, so the first key is the one to drop.
|
|
2321
|
+
senders.delete(sender);
|
|
2322
|
+
senders.set(sender, history);
|
|
2323
|
+
if (senders.size > THREAD_SENDER_CAPACITY) {
|
|
2324
|
+
const oldest = senders.keys().next();
|
|
2325
|
+
if (!oldest.done) {
|
|
2326
|
+
senders.delete(oldest.value);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
senderAddressees.delete(key);
|
|
2330
|
+
senderAddressees.set(key, senders);
|
|
2331
|
+
if (senderAddressees.size > THREAD_ADDRESSEE_CAPACITY) {
|
|
2332
|
+
const oldest = senderAddressees.keys().next();
|
|
2333
|
+
if (!oldest.done) {
|
|
2334
|
+
senderAddressees.delete(oldest.value);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
/**
|
|
2339
|
+
* The agent `sender` last addressed in thread `key` before `ts`, if this
|
|
2340
|
+
* process saw it. A reply older than every mention still remembered for
|
|
2341
|
+
* that sender has nothing to go on, and falls back to the owner.
|
|
2342
|
+
*/
|
|
2343
|
+
const senderAddresseeAt = (key, sender, ts) => {
|
|
2344
|
+
const history = senderAddressees.get(key)?.get(sender);
|
|
2345
|
+
if (!history) {
|
|
2346
|
+
return undefined;
|
|
2347
|
+
}
|
|
2348
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
2349
|
+
if (history[i].ts < ts) {
|
|
2350
|
+
return history[i].agentId;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return undefined;
|
|
2354
|
+
};
|
|
2355
|
+
/** Whose an untagged reply from `sender` at `ts` is: who they last addressed, else the owner. */
|
|
2356
|
+
const replyTargetAt = (key, sender, ts) => senderAddresseeAt(key, sender, ts) ?? ownerAt(key, ts);
|
|
2296
2357
|
/**
|
|
2297
2358
|
* One cold verdict per message — see `followUpWinner`. Bounded, because
|
|
2298
2359
|
* an entry outlives its own resolution on purpose; an entry that has not
|
|
@@ -2813,69 +2874,6 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
2813
2874
|
* nothing, which is every soul written before there was a choice.
|
|
2814
2875
|
*/
|
|
2815
2876
|
const listensOf = (agentId) => gatewayRef?.agents().find((agent) => agent.id === agentId)?.listens ?? 'thread';
|
|
2816
|
-
/**
|
|
2817
|
-
* Whether an agent that judges is still paying attention to a thread:
|
|
2818
|
-
* within `ATTENTION_MESSAGES` and `ATTENTION_MS` of when it last
|
|
2819
|
-
* answered a message that addressed it, both read from the session
|
|
2820
|
-
* rather than remembered here, so a restart forgets nothing. Anchored
|
|
2821
|
-
* to an addressed answer and not to any reply: a reply the agent chose
|
|
2822
|
-
* to give on a turn nobody asked for does not re-arm it, or a talkative
|
|
2823
|
-
* judge would keep itself attentive for good and cost a call per message
|
|
2824
|
-
* — the self-extension the spec declined. An agent never addressed in
|
|
2825
|
-
* the thread is not attentive: it was mentioned and its first turn has
|
|
2826
|
-
* not answered yet, or answered with nothing. Measured against the
|
|
2827
|
-
* message's own timestamp, not the clock, so a delivery that ran late is
|
|
2828
|
-
* judged as of when it was said. Bounded below as well as above: a
|
|
2829
|
-
* message said before the answer the window starts from is outside it
|
|
2830
|
-
* — Slack redelivers events a restarted daemon has no memory of, and a
|
|
2831
|
-
* negative age would otherwise pass the minutes bound however old the
|
|
2832
|
-
* message, on a turn the agent's answer already came after.
|
|
2833
|
-
*/
|
|
2834
|
-
const attentive = (routing, messageTs, pending) => {
|
|
2835
|
-
if (routing.lastAnsweredAt === undefined) {
|
|
2836
|
-
return false;
|
|
2837
|
-
}
|
|
2838
|
-
const answeredAt = Date.parse(routing.lastAnsweredAt);
|
|
2839
|
-
const saidAt = Number(messageTs) * 1000;
|
|
2840
|
-
if (Number.isNaN(answeredAt) || Number.isNaN(saidAt) || saidAt < answeredAt || saidAt - answeredAt > ATTENTION_MS) {
|
|
2841
|
-
return false;
|
|
2842
|
-
}
|
|
2843
|
-
return (routing.heardSinceAnswered ?? 0) + pending < ATTENTION_MESSAGES;
|
|
2844
|
-
};
|
|
2845
|
-
/**
|
|
2846
|
-
* Messages this adapter has dispatched to a session that the session
|
|
2847
|
-
* does not hold yet. The store counts what it holds, and a burst of
|
|
2848
|
-
* messages typed inside one second would each read the same count and
|
|
2849
|
-
* each be judged — the bound the window exists to give, blown by exactly
|
|
2850
|
-
* the thread that needs it. The renderer queue already knows: a renderer
|
|
2851
|
-
* is queued at dispatch and its turn starts (`turnStarted`) only once
|
|
2852
|
-
* the runner has saved the message — the same moment the store's count
|
|
2853
|
-
* takes it over — so the two never count one message twice.
|
|
2854
|
-
*/
|
|
2855
|
-
const pendingFor = (sessionId) => (renderers.get(sessionId) ?? []).filter((renderer) => !renderer.turnStarted).length
|
|
2856
|
-
+ (pendingObserves.get(sessionId) ?? 0);
|
|
2857
|
-
/**
|
|
2858
|
-
* Overhears placed on the gateway's chain and not yet written, per
|
|
2859
|
-
* session: the other kind of message the store does not hold yet. An
|
|
2860
|
-
* observe queued behind a running turn is heard only when the turn ends,
|
|
2861
|
-
* and a burst of messages to a colleague would otherwise count toward
|
|
2862
|
-
* nothing until then. Counted from placement to settlement.
|
|
2863
|
-
*/
|
|
2864
|
-
const pendingObserves = new Map();
|
|
2865
|
-
const placeObserve = (sessionId, observed) => {
|
|
2866
|
-
pendingObserves.set(sessionId, (pendingObserves.get(sessionId) ?? 0) + 1);
|
|
2867
|
-
const release = () => {
|
|
2868
|
-
const left = (pendingObserves.get(sessionId) ?? 1) - 1;
|
|
2869
|
-
if (left > 0) {
|
|
2870
|
-
pendingObserves.set(sessionId, left);
|
|
2871
|
-
}
|
|
2872
|
-
else {
|
|
2873
|
-
pendingObserves.delete(sessionId);
|
|
2874
|
-
}
|
|
2875
|
-
};
|
|
2876
|
-
void observed.then(release, release);
|
|
2877
|
-
return observed;
|
|
2878
|
-
};
|
|
2879
2877
|
/**
|
|
2880
2878
|
* Run `work` after everything already queued for this session's intake,
|
|
2881
2879
|
* and hold the next link until it settles. The link is claimed
|
|
@@ -2997,7 +2995,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
2997
2995
|
// whoever installed it.
|
|
2998
2996
|
const author = await displayNameFor(hearer, speaker.botUserId);
|
|
2999
2997
|
const observed = gateway.observe?.({ sessionId, agentId: hearer.config.agentId, message: `${author}: ${text}`, metadata: { ...metadata, [SENDER_TRUST_METADATA_KEY]: trust } });
|
|
3000
|
-
return { observed
|
|
2998
|
+
return { observed };
|
|
3001
2999
|
});
|
|
3002
3000
|
await observed;
|
|
3003
3001
|
}
|
|
@@ -3061,7 +3059,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
3061
3059
|
}
|
|
3062
3060
|
const author = await displayNameFor(connection, connection.botUserId);
|
|
3063
3061
|
const observed = gateway.observe?.({ sessionId, agentId, message: `${author} (your own post, sent from another session): ${text}`, metadata });
|
|
3064
|
-
return { observed
|
|
3062
|
+
return { observed };
|
|
3065
3063
|
});
|
|
3066
3064
|
// Tracked, so stop() drains it: a restart right after a report must not
|
|
3067
3065
|
// close the stores while the thread's session is still to hear it.
|
|
@@ -3161,6 +3159,55 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
3161
3159
|
},
|
|
3162
3160
|
};
|
|
3163
3161
|
};
|
|
3162
|
+
/**
|
|
3163
|
+
* Channel or DM, as this agent's app sees a conversation, for the
|
|
3164
|
+
* operator's standing channel authorization (`approvals.channels`), which
|
|
3165
|
+
* covers channels only. Asked of Slack (`conversations.info`), never read
|
|
3166
|
+
* off the id: a `G…` id is a private channel or a group DM depending on
|
|
3167
|
+
* its age. A definite answer is kept, since a conversation never turns
|
|
3168
|
+
* from one into the other; a failed lookup is not, and reads as unknown,
|
|
3169
|
+
* so the call asks.
|
|
3170
|
+
*/
|
|
3171
|
+
const conversationKinds = new Map();
|
|
3172
|
+
const CONVERSATION_KIND_CACHE_LIMIT = 1000;
|
|
3173
|
+
const conversationKindFor = async (agentId, conversation) => {
|
|
3174
|
+
const id = conversation.trim();
|
|
3175
|
+
const connection = connectionFor(agentId);
|
|
3176
|
+
if (!id || !connection) {
|
|
3177
|
+
return undefined;
|
|
3178
|
+
}
|
|
3179
|
+
const key = `${agentId}:${botIdentities.get(agentId)?.teamId ?? ''}:${id}`;
|
|
3180
|
+
const known = conversationKinds.get(key);
|
|
3181
|
+
if (known !== undefined) {
|
|
3182
|
+
return known;
|
|
3183
|
+
}
|
|
3184
|
+
let info;
|
|
3185
|
+
try {
|
|
3186
|
+
info = (await connection.web.conversations.info({ channel: id })).channel;
|
|
3187
|
+
}
|
|
3188
|
+
catch {
|
|
3189
|
+
return undefined;
|
|
3190
|
+
}
|
|
3191
|
+
if (!info?.id) {
|
|
3192
|
+
return undefined;
|
|
3193
|
+
}
|
|
3194
|
+
// A DM flag wins. Otherwise a channel only when Slack says so, by a
|
|
3195
|
+
// channel flag or by stating it is not a DM: a reply with none of
|
|
3196
|
+
// them is no proof of a channel.
|
|
3197
|
+
const kind = info.is_im === true || info.is_mpim === true
|
|
3198
|
+
? 'direct'
|
|
3199
|
+
: info.is_channel === true || info.is_group === true || (info.is_im === false && info.is_mpim === false) ? 'channel' : undefined;
|
|
3200
|
+
if (kind !== undefined) {
|
|
3201
|
+
if (conversationKinds.size >= CONVERSATION_KIND_CACHE_LIMIT) {
|
|
3202
|
+
const oldest = conversationKinds.keys().next().value;
|
|
3203
|
+
if (oldest !== undefined) {
|
|
3204
|
+
conversationKinds.delete(oldest);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
conversationKinds.set(key, kind);
|
|
3208
|
+
}
|
|
3209
|
+
return kind;
|
|
3210
|
+
};
|
|
3164
3211
|
/**
|
|
3165
3212
|
* The read side of the same boundary `resolveOutbound` draws: a channel
|
|
3166
3213
|
* the agent's own app is a member of, public or private. DMs and group
|
|
@@ -3334,8 +3381,21 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
3334
3381
|
const sameChannel = metadata.channel === 'slack' && typeof metadata.slackChannel === 'string'
|
|
3335
3382
|
? metadata.slackChannel
|
|
3336
3383
|
: undefined;
|
|
3337
|
-
|
|
3338
|
-
|
|
3384
|
+
// A scheduled firing has no conversation of its own. With no approval
|
|
3385
|
+
// channel configured it used to be denied at its first gated call, seen
|
|
3386
|
+
// only in the daemon log (card 218). Its schedule's destination is a
|
|
3387
|
+
// conversation a person approved as where it speaks, and only approvers
|
|
3388
|
+
// can answer there, so that is where it is asked. A configured approval
|
|
3389
|
+
// channel still wins: the operator chose it for exactly this. Another
|
|
3390
|
+
// channel's destination is not a Slack conversation id.
|
|
3391
|
+
const scheduled = event.scheduleDestination?.channel.trim().toLowerCase() === 'slack'
|
|
3392
|
+
? event.scheduleDestination
|
|
3393
|
+
: undefined;
|
|
3394
|
+
const viaSchedule = !sameChannel && !connection.config.approvalChannel ? scheduled : undefined;
|
|
3395
|
+
const channel = sameChannel ?? connection.config.approvalChannel ?? viaSchedule?.to;
|
|
3396
|
+
const thread = sameChannel
|
|
3397
|
+
? (typeof metadata.slackThread === 'string' ? metadata.slackThread : undefined)
|
|
3398
|
+
: viaSchedule?.thread;
|
|
3339
3399
|
// Undeliverable, not decided: nobody was asked, so filing this next to
|
|
3340
3400
|
// the denials somebody actually made would make the audit record lie
|
|
3341
3401
|
// about which is which.
|
|
@@ -4124,7 +4184,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
4124
4184
|
* Slack timestamps compare as strings: `seconds.microseconds`, both
|
|
4125
4185
|
* parts fixed width, so lexicographic order is chronological order.
|
|
4126
4186
|
*/
|
|
4127
|
-
const rememberAddressee = (key, agentId, ts, takes = false) => {
|
|
4187
|
+
const rememberAddressee = (key, agentId, ts, takes = false, cold = false) => {
|
|
4128
4188
|
const handovers = threadAddressee.get(key) ?? [];
|
|
4129
4189
|
let at = handovers.length;
|
|
4130
4190
|
while (at > 0 && handovers[at - 1].ts > ts) {
|
|
@@ -4135,9 +4195,13 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
4135
4195
|
if (takes) {
|
|
4136
4196
|
before.takes = true;
|
|
4137
4197
|
}
|
|
4198
|
+
// Seen live as well: no longer only a reconstruction.
|
|
4199
|
+
if (!cold) {
|
|
4200
|
+
delete before.cold;
|
|
4201
|
+
}
|
|
4138
4202
|
return;
|
|
4139
4203
|
}
|
|
4140
|
-
handovers.splice(at, 0,
|
|
4204
|
+
handovers.splice(at, 0, { agentId, ts, ...(takes ? { takes } : {}), ...(cold ? { cold } : {}) });
|
|
4141
4205
|
// Re-inserted rather than mutated in place: a Map iterates in insertion
|
|
4142
4206
|
// order, which is what makes the first key the one to evict.
|
|
4143
4207
|
threadAddressee.delete(key);
|
|
@@ -4158,15 +4222,29 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
4158
4222
|
* handover at or before it. Undefined when this process has no handover
|
|
4159
4223
|
* that old, which sends the question to the sessions.
|
|
4160
4224
|
*/
|
|
4225
|
+
/**
|
|
4226
|
+
* Whose a stop at `ts` is, from the live record: who was asked last as
|
|
4227
|
+
* of `ts` (`holderAt`). Undefined when the record cannot say, either
|
|
4228
|
+
* because this process has seen nobody asked here by `ts`, or because the
|
|
4229
|
+
* newest entry by then was rebuilt from a thread read (card 508): a
|
|
4230
|
+
* mention since would not be among them. The stop is then left to a
|
|
4231
|
+
* fresh read of the thread, never sent to the owner on a guess.
|
|
4232
|
+
*/
|
|
4233
|
+
const stopHolderAt = (key, ts) => holderAt(key, ts);
|
|
4161
4234
|
const holderAt = (key, ts) => {
|
|
4162
4235
|
const handovers = threadAddressee.get(key);
|
|
4163
4236
|
if (!handovers) {
|
|
4164
4237
|
return undefined;
|
|
4165
4238
|
}
|
|
4239
|
+
// The newest entry at or before `ts` answers, and only if this process
|
|
4240
|
+
// saw it live. A claim rebuilt from a thread read is a post; who was
|
|
4241
|
+
// asked since may be a mention it cannot see, so it leaves the question
|
|
4242
|
+
// to a fresh read. And it is newer than every live entry before it, so
|
|
4243
|
+
// skipping it for one of those would answer with a claim it superseded.
|
|
4166
4244
|
for (let index = handovers.length - 1; index >= 0; index -= 1) {
|
|
4167
4245
|
const handover = handovers[index];
|
|
4168
4246
|
if (handover && handover.ts <= ts) {
|
|
4169
|
-
return handover.agentId;
|
|
4247
|
+
return handover.cold === true ? undefined : handover.agentId;
|
|
4170
4248
|
}
|
|
4171
4249
|
}
|
|
4172
4250
|
return undefined;
|
|
@@ -4402,7 +4480,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
4402
4480
|
const own = message.user === undefined ? undefined : byBotUser.get(message.user);
|
|
4403
4481
|
if (own !== undefined) {
|
|
4404
4482
|
place(own, at);
|
|
4405
|
-
posts.push({ agentId: own, at, asked: asked.has(own) });
|
|
4483
|
+
posts.push({ agentId: own, at, ts: message.ts, asked: asked.has(own) });
|
|
4406
4484
|
// An agent here naming a colleague that answers its mentions
|
|
4407
4485
|
// asks it, as `admitAgentMention` has it live, so the answer is
|
|
4408
4486
|
// not a judge speaking unasked.
|
|
@@ -4692,13 +4770,37 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
4692
4770
|
// takes nothing.
|
|
4693
4771
|
const ownerId = owner && !tiedAt && !(read.foreign < owner.at) ? owner.agentId : undefined;
|
|
4694
4772
|
let taken;
|
|
4773
|
+
const takeovers = [];
|
|
4695
4774
|
for (const post of read.posts) {
|
|
4696
4775
|
const takes = post.agentId === ownerId || (!post.asked && listensOf(post.agentId) === 'judge');
|
|
4697
|
-
if (takes
|
|
4698
|
-
|
|
4776
|
+
if (takes) {
|
|
4777
|
+
takeovers.push(post);
|
|
4778
|
+
if (!taken || post.at > taken.at) {
|
|
4779
|
+
taken = post;
|
|
4780
|
+
}
|
|
4699
4781
|
}
|
|
4700
4782
|
}
|
|
4783
|
+
// Once the owner is known, the live record learns what the thread read
|
|
4784
|
+
// found, as it would have live: who owns the thread, and every post
|
|
4785
|
+
// that moved it, where it sits. With only the verdict, a judge's reply
|
|
4786
|
+
// to this message became the first claim this process saw and read as
|
|
4787
|
+
// the owner, so the real owner answering a mention could not take the
|
|
4788
|
+
// thread back (Codex 4290051968). Every take rather than the latest, so
|
|
4789
|
+
// a message delivered late is placed where it was typed, and the judge's
|
|
4790
|
+
// take at its own post, so a reply typed before its answer lands still
|
|
4791
|
+
// goes to the judge, as this verdict says.
|
|
4792
|
+
const warm = () => {
|
|
4793
|
+
if (ownerId === undefined) {
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4796
|
+
const key = threadKeyFor(parts);
|
|
4797
|
+
noteOrigin(key, ownerId);
|
|
4798
|
+
for (const post of takeovers) {
|
|
4799
|
+
rememberAddressee(key, post.agentId, post.ts, true, true);
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4701
4802
|
if (taken && taken.agentId !== ownerId && (ownerId !== undefined || taken.at > read.foreign)) {
|
|
4803
|
+
warm();
|
|
4702
4804
|
return taken.agentId;
|
|
4703
4805
|
}
|
|
4704
4806
|
if (!owner) {
|
|
@@ -4707,7 +4809,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
4707
4809
|
if (ownerId === undefined) {
|
|
4708
4810
|
return undefined;
|
|
4709
4811
|
}
|
|
4710
|
-
|
|
4812
|
+
warm();
|
|
4711
4813
|
return ownerId;
|
|
4712
4814
|
};
|
|
4713
4815
|
/**
|
|
@@ -5048,6 +5150,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5048
5150
|
// Ava's transcript; only the map learns who holds the thread.
|
|
5049
5151
|
if (threadKey && handoverTo) {
|
|
5050
5152
|
rememberAddressee(threadKey, handoverTo, event.ts);
|
|
5153
|
+
rememberSenderAddressee(threadKey, sender, handoverTo, event.ts);
|
|
5051
5154
|
}
|
|
5052
5155
|
// A home-channel message hands its new thread to the channel's owner
|
|
5053
5156
|
// the way a mention would, and is recorded by every socket for the same
|
|
@@ -5109,7 +5212,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5109
5212
|
const refusedStop = (connection.config.approvers ?? []).includes(sender) && isStopRequest(text, connection.botUserId);
|
|
5110
5213
|
const held = threadKey === undefined
|
|
5111
5214
|
? undefined
|
|
5112
|
-
:
|
|
5215
|
+
: refusedStop ? stopHolderAt(threadKey, event.ts) : replyTargetAt(threadKey, sender, event.ts);
|
|
5113
5216
|
if (addressed || held === connection.config.agentId) {
|
|
5114
5217
|
if (!alreadySeen(eventKey)) {
|
|
5115
5218
|
log(refusal);
|
|
@@ -5197,12 +5300,12 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5197
5300
|
return { ...admission, overhear: true };
|
|
5198
5301
|
}
|
|
5199
5302
|
// An untagged reply. How this agent listens decides what it is: an
|
|
5200
|
-
// agent that answers only mentions hears it and nothing more
|
|
5201
|
-
//
|
|
5202
|
-
//
|
|
5203
|
-
//
|
|
5204
|
-
//
|
|
5205
|
-
//
|
|
5303
|
+
// agent that answers only mentions hears it and nothing more. One that
|
|
5304
|
+
// judges takes a turn nobody asked for on it — one it may stay quiet
|
|
5305
|
+
// on — when the reply is its own by the rule every agent shares (card
|
|
5306
|
+
// 541): the agent this person last addressed, else the thread's owner.
|
|
5307
|
+
// No window: an owner an hour past its last answer still hears an
|
|
5308
|
+
// instruction as its own, and exactly one agent takes each reply.
|
|
5206
5309
|
const listens = listensOf(connection.config.agentId);
|
|
5207
5310
|
if (listens === 'mentions' || listens === 'judge') {
|
|
5208
5311
|
// Except a stop (card 337): the holder rule decides whose it is, as
|
|
@@ -5213,13 +5316,24 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5213
5316
|
(connection.config.approvers ?? []).includes(sender) &&
|
|
5214
5317
|
isStopRequest(text, connection.botUserId);
|
|
5215
5318
|
const stopHolder = approverStop ? holderAt(threadKey, event.ts) : undefined;
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5319
|
+
if (listens === 'judge' && !approverStop && threadKey !== undefined) {
|
|
5320
|
+
const target = replyTargetAt(threadKey, sender, event.ts);
|
|
5321
|
+
if (target === undefined) {
|
|
5322
|
+
// Nothing seen here says whose thread this is: the sessions and
|
|
5323
|
+
// the thread read decide, as for any agent.
|
|
5324
|
+
return { ...admission, overhear: true, judge: true, settled: false, judgeReason: 'owner, from the thread read' };
|
|
5325
|
+
}
|
|
5326
|
+
if (target !== connection.config.agentId) {
|
|
5327
|
+
return { ...admission, overhear: true, judgeReason: `${target}'s reply` };
|
|
5328
|
+
}
|
|
5329
|
+
const reason = senderAddresseeAt(threadKey, sender, event.ts) === target ? 'addressed it last' : 'owner';
|
|
5330
|
+
return { ...admission, overhear: true, judge: true, judgeReason: reason };
|
|
5331
|
+
}
|
|
5332
|
+
// An approver's stop is never judged (card 541): it stops the turn of
|
|
5333
|
+
// whoever holds the thread, and with nothing to stop it is only heard.
|
|
5219
5334
|
return {
|
|
5220
5335
|
...admission,
|
|
5221
5336
|
overhear: true,
|
|
5222
|
-
...(listens === 'judge' && !stopForAnother ? { judge: true } : {}),
|
|
5223
5337
|
...(stopHolder === connection.config.agentId ? { holderStop: true } : {}),
|
|
5224
5338
|
...(approverStop && stopHolder === undefined ? { settled: false } : {}),
|
|
5225
5339
|
};
|
|
@@ -5234,7 +5348,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5234
5348
|
isStopRequest(text, connection.botUserId);
|
|
5235
5349
|
const holder = threadKey === undefined
|
|
5236
5350
|
? undefined
|
|
5237
|
-
:
|
|
5351
|
+
: stopRequest ? stopHolderAt(threadKey, event.ts) : replyTargetAt(threadKey, sender, event.ts);
|
|
5238
5352
|
if (holder !== undefined && threadKey !== undefined) {
|
|
5239
5353
|
if (holder !== connection.config.agentId) {
|
|
5240
5354
|
// Standing down for the holder, whether or not the holder can still
|
|
@@ -5338,29 +5452,13 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5338
5452
|
// not to be this agent's leaves an empty link behind, which is all it
|
|
5339
5453
|
// should cost.
|
|
5340
5454
|
let overhear = admitted.overhear;
|
|
5341
|
-
// Judged, not ruled
|
|
5342
|
-
//
|
|
5343
|
-
//
|
|
5344
|
-
// there, and the message is heard rather than judged, which is the
|
|
5345
|
-
// safe side. A host that cannot say leaves the agent hearing.
|
|
5455
|
+
// Judged, not ruled (card 541): the intake already placed this reply
|
|
5456
|
+
// with this agent, so it runs a turn nobody asked for, which may stay
|
|
5457
|
+
// quiet. A reply only the sessions can place waits for them below.
|
|
5346
5458
|
let judged = false;
|
|
5347
|
-
if (admitted.judge &&
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
// other way round, and a window that runs one message short in a
|
|
5351
|
-
// race is a bound kept; one that runs a message long is not.
|
|
5352
|
-
const pending = pendingFor(sessionId);
|
|
5353
|
-
let routing;
|
|
5354
|
-
try {
|
|
5355
|
-
routing = await gateway.sessionRouting(sessionId);
|
|
5356
|
-
}
|
|
5357
|
-
catch (error) {
|
|
5358
|
-
warn(`slack: could not read whether ${connection.config.agentId} is attentive in ${event.channel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5359
|
-
}
|
|
5360
|
-
if (routing && attentive(routing, event.ts, pending)) {
|
|
5361
|
-
judged = true;
|
|
5362
|
-
overhear = false;
|
|
5363
|
-
}
|
|
5459
|
+
if (admitted.judge && admitted.settled) {
|
|
5460
|
+
judged = true;
|
|
5461
|
+
overhear = false;
|
|
5364
5462
|
}
|
|
5365
5463
|
if (!admitted.settled) {
|
|
5366
5464
|
// One verdict for this message, shared with whichever other agents
|
|
@@ -5382,6 +5480,10 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5382
5480
|
overhear = true;
|
|
5383
5481
|
}
|
|
5384
5482
|
else {
|
|
5483
|
+
if (admitted.judge) {
|
|
5484
|
+
judged = true;
|
|
5485
|
+
overhear = false;
|
|
5486
|
+
}
|
|
5385
5487
|
if (threadKey) {
|
|
5386
5488
|
// Answered from the store rather than from memory: hold it, so
|
|
5387
5489
|
// the rest of the thread is ordered by receipt like any other.
|
|
@@ -5397,6 +5499,12 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5397
5499
|
}
|
|
5398
5500
|
}
|
|
5399
5501
|
}
|
|
5502
|
+
// One line per decision a judging agent makes on an untagged reply
|
|
5503
|
+
// (card 541): whether it took the turn, and why. Never the text.
|
|
5504
|
+
if (admitted.judgeReason !== undefined) {
|
|
5505
|
+
const reason = admitted.settled || judged ? admitted.judgeReason : 'another agent\'s, from the thread read';
|
|
5506
|
+
log(`slack: judge: ${connection.config.agentId} ${judged ? 'took' : 'heard'} ${event.ts} in ${event.channel} (${reason})`);
|
|
5507
|
+
}
|
|
5400
5508
|
const cleaned = await humanizeMentions(connection, event.text ?? '');
|
|
5401
5509
|
// Fetched inside the chain, like the lookups below: it is network
|
|
5402
5510
|
// I/O, and a message whose screenshot is slow to arrive must not lose
|
|
@@ -5500,7 +5608,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5500
5608
|
// an overhear queued behind a long turn would hold the next
|
|
5501
5609
|
// message's place in this chain hostage to that turn.
|
|
5502
5610
|
return {
|
|
5503
|
-
observed:
|
|
5611
|
+
observed: gateway.observe({ sessionId, agentId: connection.config.agentId, message: userMessage, metadata }),
|
|
5504
5612
|
};
|
|
5505
5613
|
}
|
|
5506
5614
|
// A turn nobody asked for opens its placeholder on its first text,
|
|
@@ -5803,6 +5911,7 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
5803
5911
|
// A channel id is unique only within its workspace, so a standing
|
|
5804
5912
|
// grant on one is bound to the team the agent's app authenticated in.
|
|
5805
5913
|
accountFor: (agentId) => botIdentities.get(agentId)?.teamId || undefined,
|
|
5914
|
+
conversationKindFor,
|
|
5806
5915
|
requestCredential,
|
|
5807
5916
|
async start(gateway) {
|
|
5808
5917
|
gatewayRef = gateway;
|
|
@@ -6018,8 +6127,8 @@ export const createSlackChannelAdapter = (options) => {
|
|
|
6018
6127
|
connections.length = 0;
|
|
6019
6128
|
approvalPosts.clear();
|
|
6020
6129
|
threadAddressee.clear();
|
|
6130
|
+
senderAddressees.clear();
|
|
6021
6131
|
coldVerdicts.clear();
|
|
6022
|
-
pendingObserves.clear();
|
|
6023
6132
|
botIdentities.clear();
|
|
6024
6133
|
homeOwners.clear();
|
|
6025
6134
|
rendering.clear();
|