@timqi/pier 0.0.29 → 0.1.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 +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/channels/slack.js
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
// Slack adapter: normalize inbound Socket Mode envelopes, render outbound turns.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// here, because there is no other behaviour to switch to. Threads cost no admin
|
|
8
|
-
// right and no group conversion on Slack, so the feature Telegram has to
|
|
9
|
-
// negotiate for is simply how this adapter always works.
|
|
10
|
-
//
|
|
11
|
-
// Three more things are Slack-specific and live only here:
|
|
12
|
-
// - Commands cannot start with `/`. The Slack client intercepts an
|
|
13
|
-
// unregistered slash command and never sends it to an app, so `stop` and
|
|
14
|
-
// `settings` are bare words instead. Registering real slash commands is
|
|
15
|
-
// deliberately not a feature: it needs manifest setup to add a second way to
|
|
16
|
-
// do what a word in the thread already does, and Slack only sends `thread_ts`
|
|
17
|
-
// for a command typed *inside* a thread, so it would be the weaker path too.
|
|
18
|
-
// - Reactions are short names (`eyes`), not codepoints; `reactions.add`
|
|
19
|
-
// rejects 👀 with `invalid_name`.
|
|
20
|
-
// - Slack redelivers anything it did not see acknowledged, so `event_id` is
|
|
21
|
-
// deduplicated.
|
|
22
|
-
//
|
|
23
|
-
// Everything policy-shaped (mention/bind gates, per-chat overrides) is in
|
|
24
|
-
// config.ts, platform-blind and shared with Telegram.
|
|
2
|
+
// Pier never posts into a channel's main flow: a conversation is
|
|
3
|
+
// `<channel>/<threadTs>` and the thread is the session. Slack-specific: the
|
|
4
|
+
// client intercepts unregistered slash commands, so `stop` and `settings` are
|
|
5
|
+
// bare words; reactions are short names (`reactions.add` rejects 👀 with
|
|
6
|
+
// `invalid_name`); unacked envelopes are redelivered, so `event_id` is deduplicated.
|
|
25
7
|
import { saveInboundAll } from "../core/inbox.js";
|
|
26
8
|
import { MAX_INBOUND_BYTES } from "../core/inbound-file.js";
|
|
27
9
|
import { awaitsTurn } from "../core/reply.js";
|
|
@@ -38,36 +20,19 @@ import { SlackOutbound } from "./slack-outbound.js";
|
|
|
38
20
|
import { readThread, slackToolAvailable } from "./slack-tool.js";
|
|
39
21
|
import { SlackPanel } from "./slack-panel.js";
|
|
40
22
|
import { context, escapeMrkdwn, offeredLabel } from "./slack-render.js";
|
|
41
|
-
/** Slack wants a short name here; the raw codepoint is an `invalid_name`. */
|
|
42
23
|
const WORKING = "eyes";
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// slow the source down — the envelope is already acked — so it bounds
|
|
46
|
-
// concurrency (open sockets, downloads) rather than the backlog itself.
|
|
24
|
+
// The envelope is already acked, so this bounds concurrency (sockets,
|
|
25
|
+
// downloads), not the backlog.
|
|
47
26
|
const MAX_ACTIVE_CHATS = 16;
|
|
48
27
|
const RECEIPT_STALE_MS = 30 * 60_000;
|
|
49
28
|
const DRAIN_TIMEOUT_MS = 5000;
|
|
50
|
-
/**
|
|
51
|
-
* How long a delivered `event_id` is remembered. Slack retries an unacked
|
|
52
|
-
* envelope for a few minutes; we ack immediately, so this only has to cover
|
|
53
|
-
* a redelivery that crossed our ack.
|
|
54
|
-
*/
|
|
29
|
+
/** Only has to cover a redelivery that crossed our immediate ack. */
|
|
55
30
|
const DEDUP_TTL_MS = 5 * 60_000;
|
|
56
31
|
const DEDUP_MAX = 2000;
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
* takes. Both halves are load-bearing, because Slack gives us no leading `/` to
|
|
60
|
-
* key on: a closed set keeps ordinary prose from being a command, and the
|
|
61
|
-
* exact arity keeps "settings are broken, please help" from opening the panel
|
|
62
|
-
* (or worse, "stop the deploy and tell me why" from aborting the turn that was
|
|
63
|
-
* about to explain). Anything longer is a sentence, and goes to the agent.
|
|
64
|
-
*/
|
|
32
|
+
/** Bare words with exact arity, since there is no leading `/` to key on: "stop
|
|
33
|
+
* the deploy and tell me why" is a sentence for the agent, not an abort. */
|
|
65
34
|
const BARE_COMMANDS = new Map([["stop", 0], ["settings", 0], ["bind", 1]]);
|
|
66
|
-
/**
|
|
67
|
-
* A Slack conversation is always `<channel>/<threadTs>` — the thread is the
|
|
68
|
-
* session. This pair is the only definition of the format; control.ts decodes
|
|
69
|
-
* with it rather than splitting on "/" itself.
|
|
70
|
-
*/
|
|
35
|
+
/** The only definition of the conversation id format; control.ts decodes with it. */
|
|
71
36
|
const conversationId = (channel, threadTs) => `${channel}/${threadTs}`;
|
|
72
37
|
export const parseConversation = (id) => {
|
|
73
38
|
const at = id.indexOf("/");
|
|
@@ -75,43 +40,20 @@ export const parseConversation = (id) => {
|
|
|
75
40
|
? { channel: id, threadTs: "" }
|
|
76
41
|
: { channel: id.slice(0, at), threadTs: id.slice(at + 1) };
|
|
77
42
|
};
|
|
78
|
-
/**
|
|
79
|
-
* The thread a message belongs to. A message already in a thread keeps it; one
|
|
80
|
-
* posted in the channel becomes the root of its own — which is what makes
|
|
81
|
-
* every request its own session without asking Slack for anything.
|
|
82
|
-
*/
|
|
43
|
+
/** A message posted in the channel becomes the root of its own thread. */
|
|
83
44
|
const threadOf = (event) => event.thread_ts ?? event.ts ?? "";
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
* changes) is noise, and `bot_message` is either our own echo or another app's.
|
|
87
|
-
* `message_share` is in because a forward is a person handing the agent
|
|
88
|
-
* something to look at; dropping it delivered the sharer's comment alone, or
|
|
89
|
-
* nothing at all when they forwarded without one.
|
|
90
|
-
*/
|
|
45
|
+
/** Everything else (joins, edits, topic changes, `bot_message`) is noise; a
|
|
46
|
+
* forward is a person handing the agent something to look at. */
|
|
91
47
|
const READABLE_SUBTYPES = new Set(["file_share", "thread_broadcast", "message_share"]);
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* Slack previewing a permalink somebody pasted — the sender did not choose to
|
|
96
|
-
* forward that message, so quoting it as if they had puts words in their mouth.
|
|
97
|
-
* A real share carries *both* flags, which is why the unfurl flag can only
|
|
98
|
-
* rule one out.
|
|
99
|
-
*/
|
|
48
|
+
/** `is_share` is the flag proper, and a `message_share` may arrive without it.
|
|
49
|
+
* `is_msg_unfurl` alone is Slack previewing a pasted permalink, which the
|
|
50
|
+
* sender did not choose to forward; a real share carries both flags. */
|
|
100
51
|
const sharesOf = (event) => (event.attachments ?? []).filter((a) => a.is_share === true || (event.subtype === "message_share" && !a.is_msg_unfurl));
|
|
101
|
-
/** A share's own uploads, which Slack hangs off the attachment. */
|
|
102
52
|
const sharedFiles = (share) => share.files ?? share.original_message?.files ?? [];
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
* eagerly. A token budget, not a Slack limit: a handful of lines is worth
|
|
106
|
-
* spending on something a human deliberately forwarded, a few hundred is not,
|
|
107
|
-
* and past this the agent gets the coordinates and decides for itself.
|
|
108
|
-
*/
|
|
53
|
+
/** A token budget, not a Slack limit: past this the agent gets the
|
|
54
|
+
* coordinates and decides for itself. */
|
|
109
55
|
const INLINE_REPLY_MAX = 30;
|
|
110
|
-
/**
|
|
111
|
-
* What the user asked for, from text a mention has already been stripped from.
|
|
112
|
-
* `/stop` is accepted for muscle memory even though Slack rarely lets one
|
|
113
|
-
* through; a bare `stop` is the form that actually arrives.
|
|
114
|
-
*/
|
|
56
|
+
/** `/stop` is accepted for muscle memory; a bare `stop` is what actually arrives. */
|
|
115
57
|
function slackCommand(text) {
|
|
116
58
|
const slash = parseCommand(text);
|
|
117
59
|
if (slash)
|
|
@@ -128,27 +70,17 @@ export class SlackChannel {
|
|
|
128
70
|
id = "slack";
|
|
129
71
|
api;
|
|
130
72
|
log;
|
|
131
|
-
/** 👀 lifecycle, durable; see receipts.ts for why it is not just a Map. */
|
|
132
73
|
receipts;
|
|
133
|
-
/** Ordering per channel, concurrency across them; see chains.ts. */
|
|
134
74
|
chains;
|
|
135
|
-
/** The inbound gate and the bind-hint throttle; see gatekeeper.ts. */
|
|
136
75
|
gate;
|
|
137
|
-
/** `event_id`s already handled, against Slack's at-least-once delivery. */
|
|
138
76
|
seen;
|
|
139
|
-
/**
|
|
77
|
+
/** Absent when no control was wired (tests). */
|
|
140
78
|
panel;
|
|
141
|
-
/** Channel kinds/names and user names, cached; see slack-directory.ts. */
|
|
142
79
|
directory;
|
|
143
|
-
/**
|
|
144
|
-
*
|
|
145
|
-
* carries no channel name, so discovery costs an API call — once per channel
|
|
146
|
-
* per process rather than once per message. A rename is picked up on restart,
|
|
147
|
-
* which is soon enough for a Console display label.
|
|
148
|
-
*/
|
|
80
|
+
/** The message event carries no channel name, so discovery costs an API call
|
|
81
|
+
* — once per channel per process. */
|
|
149
82
|
discovered = new Set();
|
|
150
83
|
me = "";
|
|
151
|
-
/** Precompiled from `me`: a leading mention, and any mention. */
|
|
152
84
|
mention;
|
|
153
85
|
out;
|
|
154
86
|
socket;
|
|
@@ -164,8 +96,7 @@ export class SlackChannel {
|
|
|
164
96
|
this.api = deps.client ?? new SlackApi(config.token, config.appToken, this.log);
|
|
165
97
|
this.out = new SlackOutbound(this.api, this.log);
|
|
166
98
|
this.receipts = new Receipts(
|
|
167
|
-
//
|
|
168
|
-
// only ever applies the one.
|
|
99
|
+
// The clear needs the reaction's name back.
|
|
169
100
|
{
|
|
170
101
|
setReaction: (channel, ts, emoji) => emoji
|
|
171
102
|
? this.api.addReaction(channel, ts, emoji)
|
|
@@ -184,20 +115,17 @@ export class SlackChannel {
|
|
|
184
115
|
const auth = await this.api.authTest();
|
|
185
116
|
this.me = auth.userId;
|
|
186
117
|
if (this.me) {
|
|
187
|
-
// A Slack user id is `[A-Z0-9]+`, so it needs no escaping
|
|
188
|
-
// these once keeps two regex compiles off the per-message path.
|
|
118
|
+
// A Slack user id is `[A-Z0-9]+`, so it needs no escaping.
|
|
189
119
|
this.mention = {
|
|
190
120
|
leading: new RegExp(`^\\s*<@${this.me}>[\\s,:-]*`),
|
|
191
121
|
any: new RegExp(`<@${this.me}>`, "g"),
|
|
192
122
|
};
|
|
193
123
|
}
|
|
194
124
|
else {
|
|
195
|
-
//
|
|
196
|
-
// every channel with require-mention on goes silent. Loud, not a debug line.
|
|
125
|
+
// Every channel with require-mention on goes silent; loud, not a debug line.
|
|
197
126
|
this.log("auth.test returned no user id: mention detection is disabled");
|
|
198
127
|
}
|
|
199
128
|
this.running = true;
|
|
200
|
-
// Best-effort and off the critical path.
|
|
201
129
|
void this.receipts.sweep(true);
|
|
202
130
|
this.socket = await this.api.connect((env) => this.onEnvelope(env, onMessage));
|
|
203
131
|
}
|
|
@@ -208,22 +136,17 @@ export class SlackChannel {
|
|
|
208
136
|
await this.chains.drain(DRAIN_TIMEOUT_MS);
|
|
209
137
|
}
|
|
210
138
|
// --- inbound ---------------------------------------------------------------
|
|
211
|
-
/**
|
|
212
|
-
* Already acknowledged by the transport. Routing is synchronous so ordering
|
|
213
|
-
* is decided here, before any await.
|
|
214
|
-
*/
|
|
139
|
+
/** Routing is synchronous so ordering is decided before any await. */
|
|
215
140
|
onEnvelope(env, onMessage) {
|
|
216
141
|
if (!this.running)
|
|
217
142
|
return;
|
|
218
|
-
// Asked on every envelope, throttled inside receipts.ts.
|
|
219
143
|
void this.receipts.sweep();
|
|
220
144
|
if (env.type === "events_api") {
|
|
221
145
|
const payload = env.payload;
|
|
222
146
|
const event = payload?.event;
|
|
223
147
|
if (!event)
|
|
224
148
|
return;
|
|
225
|
-
// `app_mention` duplicates a `message.channels`
|
|
226
|
-
// its own event_id, so dedup cannot save us — it has to be ignored here.
|
|
149
|
+
// `app_mention` duplicates a `message.channels` under its own event_id.
|
|
227
150
|
if (event.type !== "message") {
|
|
228
151
|
this.log(`ignored event type ${event.type}`);
|
|
229
152
|
return;
|
|
@@ -240,8 +163,7 @@ export class SlackChannel {
|
|
|
240
163
|
const interaction = env.payload;
|
|
241
164
|
if (!interaction)
|
|
242
165
|
return;
|
|
243
|
-
// A modal submission carries its conversation in private_metadata
|
|
244
|
-
// is not tied to a channel chain.
|
|
166
|
+
// A modal submission carries its conversation in private_metadata.
|
|
245
167
|
const channel = interaction.channel?.id ?? "modal";
|
|
246
168
|
this.chains.run(channel, () => this.onInteraction(interaction, onMessage));
|
|
247
169
|
return;
|
|
@@ -249,7 +171,6 @@ export class SlackChannel {
|
|
|
249
171
|
this.log(`ignored envelope type ${env.type}`);
|
|
250
172
|
}
|
|
251
173
|
async onMessage(event, onMessage) {
|
|
252
|
-
// Our own echo, another app, or a subtype that is not a person talking.
|
|
253
174
|
if (event.bot_id || !event.user || event.user === this.me)
|
|
254
175
|
return;
|
|
255
176
|
if (event.subtype && !READABLE_SUBTYPES.has(event.subtype)) {
|
|
@@ -262,22 +183,17 @@ export class SlackChannel {
|
|
|
262
183
|
return this.log("message event without a ts, dropped");
|
|
263
184
|
const raw = (event.text ?? "").trim();
|
|
264
185
|
const shares = sharesOf(event);
|
|
265
|
-
// A share's files are in the attachment, so they join the event's own and
|
|
266
|
-
// ride the one save loop below — size gate and lost markers included.
|
|
267
186
|
const files = [...(event.files ?? []), ...shares.flatMap(sharedFiles)];
|
|
268
|
-
// A forward with no comment of its own is still content, and the whole
|
|
269
|
-
// point of the message.
|
|
270
187
|
if (!raw && !files.length && !shares.length) {
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// whose attachment `sharesOf` ruled out. Saying so beats vanishing (5b).
|
|
188
|
+
// An opted-in subtype with nothing readable is a shape this adapter did
|
|
189
|
+
// not recognize, not an empty message (§5).
|
|
274
190
|
if (event.subtype)
|
|
275
191
|
this.log(`${event.subtype} with nothing readable in it, dropped`);
|
|
276
192
|
return;
|
|
277
193
|
}
|
|
278
194
|
const { kind } = await this.directory.channel(this.api, channel, event);
|
|
279
195
|
const isDm = kind === "dm";
|
|
280
|
-
if (!this.discovered.has(channel)) {
|
|
196
|
+
if (!this.discovered.has(channel) && this.gate.mayDiscover({ isDm, userId: event.user })) {
|
|
281
197
|
this.discovered.add(channel);
|
|
282
198
|
const name = await this.nameOf(channel, event);
|
|
283
199
|
this.deps.store.discoverChat("slack", { id: channel, name, kind });
|
|
@@ -302,37 +218,30 @@ export class SlackChannel {
|
|
|
302
218
|
return this.bind(channel, event.user, threadTs, command?.args ?? "");
|
|
303
219
|
if (command?.name === "stop")
|
|
304
220
|
return this.abortTurn(here, channel, threadTs);
|
|
305
|
-
// `@bot`
|
|
306
|
-
// `settings` are the same request: show me this conversation's settings.
|
|
221
|
+
// A bare `@bot` and `settings` are the same request.
|
|
307
222
|
if (this.panel && (command?.name === "settings" || (!text && !files.length && !shares.length))) {
|
|
308
223
|
return this.panel.open(here, channel, threadTs);
|
|
309
224
|
}
|
|
310
|
-
// Downloading only past the gate: an unauthorized sender must not
|
|
311
|
-
//
|
|
225
|
+
// Downloading only past the gate: an unauthorized sender must not make the
|
|
226
|
+
// bot pull bytes on their behalf.
|
|
312
227
|
const markers = await this.saveAttachments(files);
|
|
313
228
|
const shared = await Promise.all(shares.map((share) => this.sharedBlock(share)));
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// *before* the mark: every await between mark() and dispatch is a window
|
|
317
|
-
// in which a previous turn can end and settle, taking this receipt with it
|
|
318
|
-
// (paid for on Lark first).
|
|
229
|
+
// Resolved before the mark: any await between mark() and dispatch is a
|
|
230
|
+
// window in which a previous turn can settle and take this receipt with it.
|
|
319
231
|
const sender = { id: event.user, name: await this.directory.user(this.api, event.user) };
|
|
320
232
|
this.receipts.mark(here.conversationId, channel, ts);
|
|
321
|
-
//
|
|
322
|
-
// is the wrong default when the human is watching a 👀 in a thread.
|
|
233
|
+
// Steer: a follow-up is the wrong default when the human is watching a 👀.
|
|
323
234
|
onMessage({
|
|
324
235
|
key: here,
|
|
325
236
|
senderId: event.user,
|
|
326
237
|
sender,
|
|
327
|
-
// Markers
|
|
238
|
+
// Markers last: the inbound-file convention is a trailing block.
|
|
328
239
|
text: [text, ...shared, ...markers].filter(Boolean).join("\n"),
|
|
329
240
|
mode: "steer",
|
|
330
241
|
});
|
|
331
242
|
}
|
|
332
243
|
// --- interactions ----------------------------------------------------------
|
|
333
244
|
async onInteraction(interaction, onMessage) {
|
|
334
|
-
// A modal submission carries the conversation in private_metadata and is
|
|
335
|
-
// answered entirely by the panel.
|
|
336
245
|
if (interaction.type === "view_submission") {
|
|
337
246
|
if (!(await this.panel?.onViewSubmission(interaction))) {
|
|
338
247
|
this.log(`unhandled view submission ${interaction.view?.callback_id ?? "?"}`);
|
|
@@ -363,24 +272,19 @@ export class SlackChannel {
|
|
|
363
272
|
});
|
|
364
273
|
if (!admitted)
|
|
365
274
|
return;
|
|
366
|
-
// Panel clicks are namespaced `cfg:` and never reach the agent.
|
|
367
275
|
if (await this.panel?.onAction(interaction, key, actionId))
|
|
368
276
|
return;
|
|
369
277
|
const text = offeredLabel(message.blocks, actionId);
|
|
370
278
|
if (text === undefined) {
|
|
371
|
-
// The person clicked and would otherwise see nothing happen (
|
|
279
|
+
// The person clicked and would otherwise see nothing happen (§5).
|
|
372
280
|
this.log(`unknown action ${actionId} in channel ${channel}`);
|
|
373
281
|
await this.api.postMessage({ channel, thread_ts: threadTs, text: STALE_OPTION })
|
|
374
282
|
.catch((err) => this.log(`stale-option notice failed: ${String(err)}`));
|
|
375
283
|
return;
|
|
376
284
|
}
|
|
377
|
-
// The options belonged to the turn that just ended; once one is taken the
|
|
378
|
-
// rest answer a question the conversation has moved past (the web drops
|
|
379
|
-
// them for the same reason).
|
|
380
285
|
await this.retireOptions(channel, message.ts, message.blocks);
|
|
381
|
-
// A bot cannot post as the user, so the pick is echoed
|
|
382
|
-
//
|
|
383
|
-
// made, and there is no message of the user's to carry the eyes.
|
|
286
|
+
// A bot cannot post as the user, so the pick is echoed: otherwise the
|
|
287
|
+
// thread shows an answer to a request nobody can see, with nothing to carry the eyes.
|
|
384
288
|
const sender = { id: user, name: await this.directory.user(this.api, user) };
|
|
385
289
|
const echo = await this.api.postMessage({
|
|
386
290
|
channel,
|
|
@@ -390,38 +294,26 @@ export class SlackChannel {
|
|
|
390
294
|
this.log(`option echo failed: ${String(err)}`);
|
|
391
295
|
return undefined;
|
|
392
296
|
});
|
|
393
|
-
//
|
|
394
|
-
// buttons: the eyes mean "this input is being worked on". No await
|
|
395
|
-
// between mark and dispatch — see onMessage.
|
|
297
|
+
// No await between mark and dispatch — see onMessage.
|
|
396
298
|
if (echo?.ts)
|
|
397
299
|
this.receipts.mark(key.conversationId, channel, echo.ts);
|
|
398
300
|
onMessage({ key, senderId: user, sender, text, mode: "steer" });
|
|
399
301
|
}
|
|
400
|
-
/**
|
|
401
|
-
*
|
|
402
|
-
* that was *nothing but* its options leaves nothing to keep, and Slack will
|
|
403
|
-
* not accept a message with neither text nor blocks — so it becomes a muted
|
|
404
|
-
* line rather than staying clickable forever.
|
|
405
|
-
*/
|
|
302
|
+
/** Slack will not accept a message with neither text nor blocks, so a turn
|
|
303
|
+
* that was nothing but its options becomes a muted line. */
|
|
406
304
|
async retireOptions(channel, ts, blocks) {
|
|
407
305
|
const kept = (blocks ?? []).filter((b) => b.type !== "actions");
|
|
408
306
|
const fallback = kept.find((b) => b.type === "section")?.text.text;
|
|
409
307
|
await this.api.setBlocks(channel, ts, fallback ?? "Option taken.", kept.length ? kept : [context("_Option taken._")]).catch((err) => this.log(`retiring options failed: ${String(err)}`));
|
|
410
308
|
}
|
|
411
|
-
/**
|
|
412
|
-
* Stop the turn this conversation is running. The abort makes Pi end the
|
|
413
|
-
* turn, which reaches send() through the normal turn-end path and clears the
|
|
414
|
-
* 👀 receipts — so nothing here touches them.
|
|
415
|
-
*/
|
|
309
|
+
/** The abort ends the turn, which reaches send() and clears the receipts. */
|
|
416
310
|
async abortTurn(key, channel, threadTs) {
|
|
417
311
|
await this.deps.control?.abort(key);
|
|
418
312
|
await this.api.postMessage({ channel, thread_ts: threadTs, text: STOPPED });
|
|
419
313
|
}
|
|
420
314
|
// --- bind ------------------------------------------------------------------
|
|
421
|
-
/**
|
|
422
|
-
*
|
|
423
|
-
* but a DM that swallows every message looks broken rather than locked.
|
|
424
|
-
*/
|
|
315
|
+
/** Channels stay silent, but a DM that swallows every message looks broken
|
|
316
|
+
* rather than locked. */
|
|
425
317
|
async hintBind(channel, userId, threadTs) {
|
|
426
318
|
if (!this.gate.mayHint(userId))
|
|
427
319
|
return;
|
|
@@ -433,36 +325,28 @@ export class SlackChannel {
|
|
|
433
325
|
}
|
|
434
326
|
async bind(channel, userId, threadTs, code) {
|
|
435
327
|
const name = await this.directory.user(this.api, userId);
|
|
436
|
-
const
|
|
328
|
+
const outcome = this.deps.store.redeemBindCode("slack", code, { id: userId, name });
|
|
437
329
|
await this.api.postMessage({
|
|
438
330
|
channel,
|
|
439
331
|
thread_ts: threadTs,
|
|
440
|
-
text: bindResult(
|
|
332
|
+
text: bindResult(outcome, escapeMrkdwn(name)),
|
|
441
333
|
});
|
|
442
334
|
}
|
|
443
335
|
// --- addressing ------------------------------------------------------------
|
|
444
|
-
/**
|
|
445
|
-
*
|
|
446
|
-
* of Telegram's "replying to the bot". The thread check is what lets a
|
|
447
|
-
* conversation flow without an `@` on every line, and it is durable so it
|
|
448
|
-
* still holds after a restart.
|
|
449
|
-
*/
|
|
336
|
+
/** Mentioned, or continuing a thread Pier already owns — durable, so it
|
|
337
|
+
* holds after a restart. */
|
|
450
338
|
addressed(raw, event, key) {
|
|
451
339
|
if (this.me && raw.includes(`<@${this.me}>`))
|
|
452
340
|
return true;
|
|
453
341
|
return !!event.thread_ts && !!this.deps.control?.knows(key);
|
|
454
342
|
}
|
|
455
|
-
/**
|
|
456
|
-
* A leading `<@BOT>` is addressing, not content — the agent should not see
|
|
457
|
-
* it. Slack does not strip it for us, and puts it wherever the user typed it.
|
|
458
|
-
*/
|
|
343
|
+
/** Slack does not strip the mention for us. */
|
|
459
344
|
stripMention(text) {
|
|
460
345
|
if (!this.mention)
|
|
461
346
|
return text;
|
|
462
347
|
return text.replace(this.mention.leading, "").replace(this.mention.any, "").trim();
|
|
463
348
|
}
|
|
464
349
|
// --- lookups ---------------------------------------------------------------
|
|
465
|
-
/** A readable label for the Console's channel list; the id is the fallback. */
|
|
466
350
|
async nameOf(channel, event) {
|
|
467
351
|
const { kind, name } = await this.directory.channel(this.api, channel, event);
|
|
468
352
|
if (kind === "dm") {
|
|
@@ -470,32 +354,20 @@ export class SlackChannel {
|
|
|
470
354
|
}
|
|
471
355
|
return name ?? channel;
|
|
472
356
|
}
|
|
473
|
-
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* channel or a ts the event does not carry is simply left out of the line.
|
|
478
|
-
*
|
|
479
|
-
* The eager read runs whether or not `agentTool` is on, because this is
|
|
480
|
-
* inbound normalization of a message a human deliberately handed the agent —
|
|
481
|
-
* the same act as an upload, which nothing gates either. `agentTool` governs
|
|
482
|
-
* the agent reaching *out*, and the only thing it changes here is the hint,
|
|
483
|
-
* which would otherwise name a tool this session does not have.
|
|
484
|
-
*/
|
|
357
|
+
/** The eager thread read runs whether or not `agentTool` is on: a human
|
|
358
|
+
* handing the agent a message is the same act as an upload, which nothing
|
|
359
|
+
* gates. `agentTool` only changes the hint, which must not name a tool this
|
|
360
|
+
* session does not have. */
|
|
485
361
|
async sharedBlock(share) {
|
|
486
362
|
const source = share.original_message;
|
|
487
363
|
const ts = share.ts ?? source?.ts;
|
|
488
364
|
const threadTs = share.thread_ts ?? source?.thread_ts ?? ts;
|
|
489
365
|
const replies = share.reply_count ?? source?.reply_count;
|
|
490
|
-
// A share of a
|
|
491
|
-
// the three fields a thread needs travel together so neither branch below
|
|
492
|
-
// has to assert they are there.
|
|
366
|
+
// A share of a reply is one message; only a parent has a thread.
|
|
493
367
|
const parent = share.channel_id && ts && threadTs === ts && replies
|
|
494
368
|
? { channel: share.channel_id, ts, replies }
|
|
495
369
|
: null;
|
|
496
|
-
// `name<id>` is the sender prefix's grammar (core/identity.ts)
|
|
497
|
-
// is the only thing a mention can be built from. Resolved through the same
|
|
498
|
-
// cache the senders use, so a shared author already seen costs nothing.
|
|
370
|
+
// `name<id>` is the sender prefix's grammar (core/identity.ts).
|
|
499
371
|
const author = share.author_id
|
|
500
372
|
? `${await this.directory.user(this.api, share.author_id)}<${share.author_id}>`
|
|
501
373
|
: share.author_name || share.author_subname;
|
|
@@ -508,42 +380,28 @@ export class SlackChannel {
|
|
|
508
380
|
where && `in ${where}`,
|
|
509
381
|
ts && `at ${ts}`,
|
|
510
382
|
].filter(Boolean).join(" ");
|
|
511
|
-
// `fallback` is
|
|
512
|
-
//
|
|
513
|
-
// Empty rather than absent is the normal case for a file-only forward, so
|
|
514
|
-
// these fall through on "" as well.
|
|
383
|
+
// `fallback` is Slack's plain-text rendering when a share's `text` is
|
|
384
|
+
// empty (a file-only forward, or a body that is all blocks); "" is normal.
|
|
515
385
|
const body = (share.text || share.fallback || source?.text || "").trim();
|
|
516
386
|
const thread = parent && parent.replies <= INLINE_REPLY_MAX
|
|
517
387
|
? await this.sharedThread(parent.channel, parent.ts, parent.replies)
|
|
518
388
|
: { transcript: false, lines: [] };
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
// guessed from a permalink. Naming the tool is a lie when the Console has
|
|
522
|
-
// switched agent access off, so only that half goes — the coordinates are
|
|
523
|
-
// true either way.
|
|
389
|
+
// The coordinates carry the tool's own parameter names; naming the tool is
|
|
390
|
+
// a lie when the Console has switched agent access off.
|
|
524
391
|
const how = slackToolAvailable(this.deps.store) ? "read with the slack tool: " : "";
|
|
525
392
|
const hint = parent && !thread.transcript
|
|
526
393
|
? `[thread: ${parent.replies} replies — ${how}channel ${parent.channel}, thread_ts ${parent.ts}]`
|
|
527
394
|
: "";
|
|
528
|
-
// The transcript opens with the shared message itself
|
|
529
|
-
// text above it would only cost tokens.
|
|
395
|
+
// The transcript opens with the shared message itself.
|
|
530
396
|
return [`[${head}]`, thread.transcript ? "" : body, ...thread.lines, hint]
|
|
531
397
|
.filter(Boolean).join("\n");
|
|
532
398
|
}
|
|
533
|
-
/**
|
|
534
|
-
*
|
|
535
|
-
* lines — through the tool's own read, so the paging, the seam dedup, the
|
|
536
|
-
* ordering and the error-to-action translation exist once (slack-tool.ts).
|
|
537
|
-
*
|
|
538
|
-
* A read that fails or comes back cut says so in the prompt (5b): a thread
|
|
539
|
-
* the agent silently never saw is indistinguishable from one with nothing in
|
|
540
|
-
* it, and the turn is dispatched either way.
|
|
541
|
-
*/
|
|
399
|
+
/** Through the slack tool's own read, so paging and dedup exist once. A read
|
|
400
|
+
* that fails or comes back cut says so in the prompt (§5). */
|
|
542
401
|
async sharedThread(channel, ts, replies) {
|
|
543
402
|
const deps = { directory: this.directory, log: this.log };
|
|
544
403
|
try {
|
|
545
|
-
//
|
|
546
|
-
// that undercounts still reports itself as cut rather than as complete.
|
|
404
|
+
// One over the budget, so an undercounting reply_count still reports as cut.
|
|
547
405
|
const read = await readThread(deps, this.api, channel, ts, undefined, INLINE_REPLY_MAX + 2);
|
|
548
406
|
if (!read.messages.length)
|
|
549
407
|
return { transcript: false, lines: [] };
|
|
@@ -552,9 +410,6 @@ export class SlackChannel {
|
|
|
552
410
|
lines: [
|
|
553
411
|
`[thread: ${replies} replies, oldest first — ${read.format}]`,
|
|
554
412
|
...read.messages,
|
|
555
|
-
// Cut either because a page failed or because the thread turned out
|
|
556
|
-
// longer than `reply_count` promised; a transcript that answers as
|
|
557
|
-
// if it were complete is the one nobody double-checks.
|
|
558
413
|
...(read.incomplete || read.truncated
|
|
559
414
|
? [`[thread partly read: ${read.incomplete ?? `cut at ${read.count} lines`}]`]
|
|
560
415
|
: []),
|
|
@@ -563,16 +418,12 @@ export class SlackChannel {
|
|
|
563
418
|
}
|
|
564
419
|
catch (err) {
|
|
565
420
|
this.log(`shared thread ${channel}/${ts} not read: ${String(err)}`);
|
|
566
|
-
// Said in the prompt in the same shape as an attachment that never made
|
|
567
|
-
// it, because it is the same fact to the reader.
|
|
568
421
|
return {
|
|
569
422
|
transcript: false,
|
|
570
423
|
lines: [`[thread not read: ${err instanceof Error ? err.message : String(err)}]`],
|
|
571
424
|
};
|
|
572
425
|
}
|
|
573
426
|
}
|
|
574
|
-
/** The upload list as the shared save loop wants it (the loop itself, size
|
|
575
|
-
* gate and lost markers included, is core/inbox.ts). */
|
|
576
427
|
saveAttachments(files) {
|
|
577
428
|
return saveInboundAll(this.id, files.map((file) => ({
|
|
578
429
|
label: file.name ?? "attachment",
|
|
@@ -584,34 +435,20 @@ export class SlackChannel {
|
|
|
584
435
|
})), this.log);
|
|
585
436
|
}
|
|
586
437
|
// --- outbound --------------------------------------------------------------
|
|
587
|
-
/**
|
|
588
|
-
* Called on every turn-end, empty text included: the turn settled with
|
|
589
|
-
* nothing to say, and the 👀 receipts still have to come off.
|
|
590
|
-
*/
|
|
591
438
|
async send(conversation, reply) {
|
|
592
439
|
const { channel, threadTs } = parseConversation(conversation);
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
// in the channel's main flow — the one thing this adapter promises never to
|
|
596
|
-
// do — so it is refused loudly instead, and the receipts still come off so
|
|
597
|
-
// no 👀 is stranded.
|
|
440
|
+
// No thread is a foreign id; posting it would put a turn in the channel's
|
|
441
|
+
// main flow. Refused loudly, receipts still cleared.
|
|
598
442
|
if (!threadTs) {
|
|
599
443
|
this.log(`refusing to answer ${conversation}: no thread in the conversation id`);
|
|
600
444
|
await this.receipts.settle(conversation);
|
|
601
445
|
return;
|
|
602
446
|
}
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
await this.receipts.settleAfter(conversation, () => this.out.reply(channel, threadTs, reply));
|
|
447
|
+
// The turn ended either way; a 👀 left up by a failed send looks like work.
|
|
448
|
+
await this.receipts.settleAfter(conversation, () => this.out.reply(channel, threadTs, reply), reply.meta);
|
|
606
449
|
}
|
|
607
|
-
/**
|
|
608
|
-
*
|
|
609
|
-
* no message of the user's to carry them — nobody typed one — so without this
|
|
610
|
-
* the thread shows nothing at all while the agent works. The turn-end `send`
|
|
611
|
-
* clears it like any other receipt; an error note is not marked, because no
|
|
612
|
-
* turn follows it (`awaitsTurn`) and the eyes would sit there until the stale
|
|
613
|
-
* sweep.
|
|
614
|
-
*/
|
|
450
|
+
/** The 👀 goes on the note itself: the turn it triggers has no message of
|
|
451
|
+
* the user's to carry them. */
|
|
615
452
|
async notify(conversation, note) {
|
|
616
453
|
const { channel, threadTs } = parseConversation(conversation);
|
|
617
454
|
if (!threadTs) {
|
|
@@ -619,9 +456,6 @@ export class SlackChannel {
|
|
|
619
456
|
return;
|
|
620
457
|
}
|
|
621
458
|
const ts = await this.out.note(channel, threadTs, note);
|
|
622
|
-
// The last chunk of a long note, so the eyes sit at the foot of the thread,
|
|
623
|
-
// where the reply will land. A reaction that fails is swallowed and logged
|
|
624
|
-
// by receipts.ts, so the note itself is never lost to one.
|
|
625
459
|
if (ts && awaitsTurn(note.origin))
|
|
626
460
|
this.receipts.mark(conversation, channel, ts);
|
|
627
461
|
}
|