allaboard 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +43 -2
- package/dist/agent/claude/backend.js +124 -0
- package/dist/agent/claude/capabilities.js +30 -0
- package/dist/agent/claude/chrome.js +42 -0
- package/dist/agent/claude/hooks.js +37 -0
- package/dist/agent/claude/info.js +61 -0
- package/dist/agent/claude/listing.js +230 -0
- package/dist/agent/claude/module.js +82 -0
- package/dist/agent/claude/prompt.js +24 -0
- package/dist/agent/claude/record.js +9 -0
- package/dist/agent/claude/requests.js +122 -0
- package/dist/agent/claude/session.js +441 -0
- package/dist/agent/claude/translate.js +581 -0
- package/dist/agent/claude/trust.js +576 -0
- package/dist/agent/claude/usage-probe.js +103 -0
- package/dist/agent/claude/usage.js +110 -0
- package/dist/agent/claude/wire.js +22 -0
- package/dist/agent/seam.js +21 -0
- package/dist/chat/seam.js +10 -0
- package/dist/chat/slack/app/answers.js +209 -0
- package/dist/chat/slack/app/app.js +262 -0
- package/dist/chat/slack/app/clip.js +191 -0
- package/dist/chat/slack/app/guards.js +162 -0
- package/dist/chat/slack/app/home-controls.js +91 -0
- package/dist/chat/slack/app/messages.js +181 -0
- package/dist/chat/slack/app/open.js +392 -0
- package/dist/chat/slack/app/order.js +37 -0
- package/dist/chat/slack/app/requests.js +482 -0
- package/dist/chat/slack/app/resume-bind.js +71 -0
- package/dist/chat/slack/app/wire.js +83 -0
- package/dist/chat/slack/attachments.js +348 -0
- package/dist/chat/slack/bind.js +67 -0
- package/dist/chat/slack/channel.js +67 -0
- package/dist/chat/slack/channels.js +43 -0
- package/dist/chat/slack/delete.js +278 -0
- package/dist/chat/slack/footer.js +43 -0
- package/dist/chat/slack/hold.js +37 -0
- package/dist/chat/slack/home.js +1069 -0
- package/dist/chat/slack/module.js +174 -0
- package/dist/chat/slack/notice.js +64 -0
- package/dist/chat/slack/openfile/modal.js +362 -0
- package/dist/chat/slack/openfile/read.js +29 -0
- package/dist/chat/slack/quiet-logger.js +52 -0
- package/dist/chat/slack/repair.js +123 -0
- package/dist/chat/slack/reply/blocks.js +372 -0
- package/dist/chat/slack/reply/chars.js +59 -0
- package/dist/chat/slack/reply/clients.js +171 -0
- package/dist/chat/slack/reply/errors.js +125 -0
- package/dist/chat/slack/reply/escape.js +25 -0
- package/dist/chat/slack/reply/limiter.js +86 -0
- package/dist/chat/slack/reply/markdown.js +210 -0
- package/dist/chat/slack/reply/places.js +262 -0
- package/dist/chat/slack/reply/sinks.js +2403 -0
- package/dist/chat/slack/reply/status.js +433 -0
- package/dist/chat/slack/reply/tasks.js +11 -0
- package/dist/chat/slack/requests.js +552 -0
- package/dist/chat/slack/resume.js +127 -0
- package/dist/chat/slack/setup.js +114 -0
- package/dist/chat/slack/texts.js +185 -0
- package/dist/chat/slack/thread.js +537 -0
- package/dist/chat/slack/update.js +71 -0
- package/dist/chat/slack/voice.js +84 -0
- package/dist/cli.js +326 -0
- package/dist/clock.js +21 -0
- package/dist/core/cleanup.js +145 -0
- package/dist/core/commands.js +172 -0
- package/dist/core/config.js +174 -0
- package/dist/core/files/changed.js +125 -0
- package/dist/core/files/listing.js +382 -0
- package/dist/core/files/paths.js +9 -0
- package/dist/core/files/read.js +191 -0
- package/dist/core/files/waiting.js +34 -0
- package/dist/core/flows/answers.js +133 -0
- package/dist/core/flows/ask-owner.js +95 -0
- package/dist/core/flows/asking.js +14 -0
- package/dist/core/flows/bind.js +95 -0
- package/dist/core/flows/contract.js +1 -0
- package/dist/core/flows/flows.js +35 -0
- package/dist/core/flows/hold.js +22 -0
- package/dist/core/flows/inbound.js +24 -0
- package/dist/core/flows/lists.js +21 -0
- package/dist/core/flows/messages.js +293 -0
- package/dist/core/flows/notices.js +51 -0
- package/dist/core/flows/repair.js +106 -0
- package/dist/core/flows/resume.js +163 -0
- package/dist/core/flows/setup.js +130 -0
- package/dist/core/flows/status.js +98 -0
- package/dist/core/flows/talk.js +207 -0
- package/dist/core/flows/update.js +370 -0
- package/dist/core/flows/words.js +171 -0
- package/dist/core/folders.js +93 -0
- package/dist/core/footer.js +313 -0
- package/dist/core/guide.js +50 -0
- package/dist/core/hold.js +126 -0
- package/dist/core/instance.js +46 -0
- package/dist/core/lock.js +412 -0
- package/dist/core/models.js +10 -0
- package/dist/core/paths.js +75 -0
- package/dist/core/port/file-flows.js +1 -0
- package/dist/core/port/home.js +1 -0
- package/dist/core/port/requests.js +1 -0
- package/dist/core/port/threads.js +1 -0
- package/dist/core/port/updates.js +1 -0
- package/dist/core/port.js +17 -0
- package/dist/core/reply/fold.js +175 -0
- package/dist/core/reply/previews.js +258 -0
- package/dist/core/reply/renderer.js +846 -0
- package/dist/core/reply/tail.js +24 -0
- package/dist/core/reply/words.js +32 -0
- package/dist/core/requests.js +158 -0
- package/dist/core/sessions/constants.js +61 -0
- package/dist/core/sessions/deps.js +1 -0
- package/dist/core/sessions/directory.js +86 -0
- package/dist/core/sessions/errors.js +79 -0
- package/dist/core/sessions/manager.js +378 -0
- package/dist/core/sessions/session.js +3297 -0
- package/dist/core/sessions/tasks.js +266 -0
- package/dist/core/sessions/turn.js +45 -0
- package/dist/core/setup.js +28 -0
- package/dist/core/state.js +724 -0
- package/dist/core/texts.js +451 -0
- package/dist/core/updates.js +332 -0
- package/dist/daemon.js +566 -0
- package/dist/host/npm.js +134 -0
- package/dist/host/program.js +49 -0
- package/dist/host/run.js +52 -0
- package/dist/host/service/launchd.js +385 -0
- package/dist/host/service/listing.js +42 -0
- package/dist/host/service/seam.js +1 -0
- package/dist/log.js +37 -0
- package/dist/main.js +29 -0
- package/dist/node-version.js +40 -0
- package/dist/registry.js +12 -0
- package/dist/semver.js +53 -0
- package/dist/text.js +89 -0
- package/dist/updates-host.js +113 -0
- package/dist/values.js +30 -0
- package/dist/version.js +54 -0
- package/package.json +32 -4
- package/slack-app-manifest.json +50 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reaction on a session's root message: working, waiting for the owner,
|
|
3
|
+
* everything ended, error, or stopped/restarted. Adding or removing a reaction on the owner's own
|
|
4
|
+
* message notifies nobody and only shows in the channel list on iOS (measured 2026-09-28, Slack
|
|
5
|
+
* free plan). Arguments and error codes read from docs.slack.dev/reference/methods/reactions.add
|
|
6
|
+
* and .../reactions.remove, 2026-09-28.
|
|
7
|
+
*
|
|
8
|
+
* And Slack's own status line under a thread's last message (`ThreadStatus`), for as long as a
|
|
9
|
+
* prompt is on its way or a turn runs. Arguments, the two minute timeout and the rate limit (600
|
|
10
|
+
* calls a minute for the app) read from
|
|
11
|
+
* docs.slack.dev/reference/methods/assistant.threads.setStatus, 2026-10-02.
|
|
12
|
+
*
|
|
13
|
+
* How the waits are made:
|
|
14
|
+
* - A cancel is an `AbortSignal`. `StatusReaction.show` takes one and looks at it while it waits
|
|
15
|
+
* for the lock and after each Slack call returns; a `WebClient` call cannot be interrupted, so
|
|
16
|
+
* a cancel lands when the call in flight is back, never in the middle of it.
|
|
17
|
+
* `ThreadStatus` gives its own task an `AbortController` that `close` aborts, with the same
|
|
18
|
+
* rule: the call in flight is awaited and its answer used.
|
|
19
|
+
* - The lock is the sessions' `Mutex`, whose wait the signal can leave.
|
|
20
|
+
* - A keeper's nap is a wake flag with one waiter, raced against `clock.sleep(delay)`; the clock
|
|
21
|
+
* is a parameter, in seconds.
|
|
22
|
+
* - The keeper yields one microtask before its first look at the state, so the code that called
|
|
23
|
+
* `show` finishes first.
|
|
24
|
+
* - The two process-wide flags are module state; `resetStatusFlags` clears them for the tests.
|
|
25
|
+
*/
|
|
26
|
+
import { Mutex, texts } from "../../../core/port.js";
|
|
27
|
+
import { getLogger } from "../../../log.js";
|
|
28
|
+
import { describe, gone } from "./errors.js";
|
|
29
|
+
export const logger = getLogger("allaboard.chat.slack.reply.status");
|
|
30
|
+
// Slack removes a status two minutes after it was set (the method's reference): set again well
|
|
31
|
+
// before that.
|
|
32
|
+
export const THREAD_STATUS_REFRESH_SECONDS = 60.0;
|
|
33
|
+
// The reference says a status is cleared "when the app sends a reply". Whether a stream append
|
|
34
|
+
// or an edit counts was not measured, so the status is set again this long after a write: at
|
|
35
|
+
// most one call every two seconds per thread while its replies are written (the method allows
|
|
36
|
+
// 600 a minute for the app), whichever way Slack treats them.
|
|
37
|
+
export const THREAD_STATUS_AFTER_WRITE_SECONDS = 2.0;
|
|
38
|
+
// Answers that say the app's token cannot call the method at all (its reference's error list).
|
|
39
|
+
export const THREAD_STATUS_REFUSED = ["missing_scope", "not_allowed_token_type"];
|
|
40
|
+
/** The reaction names, in the order of a strip. */
|
|
41
|
+
export const Status = {
|
|
42
|
+
WORKING: "hourglass_flowing_sand",
|
|
43
|
+
WAITING: "raised_hand",
|
|
44
|
+
DONE: "white_check_mark",
|
|
45
|
+
ERROR: "x",
|
|
46
|
+
};
|
|
47
|
+
/** The reaction that shows each status of a session on its root message. */
|
|
48
|
+
export const REACTIONS = {
|
|
49
|
+
working: Status.WORKING,
|
|
50
|
+
waiting: Status.WAITING,
|
|
51
|
+
done: Status.DONE,
|
|
52
|
+
error: Status.ERROR,
|
|
53
|
+
};
|
|
54
|
+
// Process-wide, once true: no instance calls Slack for reactions again this run.
|
|
55
|
+
let missingScope = false;
|
|
56
|
+
// Process-wide, once true: no instance calls Slack for a thread status again this run.
|
|
57
|
+
let refusedStatus = false;
|
|
58
|
+
/** Forget both "stop for the rest of the run" flags: for the tests. */
|
|
59
|
+
export function resetStatusFlags() {
|
|
60
|
+
missingScope = false;
|
|
61
|
+
refusedStatus = false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Keeps one reaction on `rootTs` in sync with the session's state. Showing the same state
|
|
65
|
+
* again makes no call. A change adds the new reaction before removing the previous one, so the
|
|
66
|
+
* root is never bare between them, and only once the add succeeded (or was already there):
|
|
67
|
+
* a failed add leaves `current` as it was, so the next `show` retries rather than stripping
|
|
68
|
+
* the root's own reaction for nothing. A fresh instance (a new session's reaction, next to
|
|
69
|
+
* whatever an earlier one already left on the same root) also strips the other three names on
|
|
70
|
+
* its own first successful add, so the root never carries more than one. Calls are serialized
|
|
71
|
+
* per instance (a lock): two quick changes end on the last one. A change is two calls,
|
|
72
|
+
* so a caller aborted between them leaves both reactions on the root: the instance then
|
|
73
|
+
* reads as a fresh one, whose next change strips the other names, and `settle` makes that
|
|
74
|
+
* change, to the state asked for last, for whoever aborted it. `already_reacted` on
|
|
75
|
+
* add and `no_reaction` on remove count as done. `missing_scope` (the workspace has not
|
|
76
|
+
* reinstalled the app for `reactions:write`) is logged once for the whole process, and every
|
|
77
|
+
* instance stops calling Slack for reactions from then on; any other failure is logged
|
|
78
|
+
* (channel, ts and the error code only) and swallowed, since a reaction must never break a
|
|
79
|
+
* turn.
|
|
80
|
+
*/
|
|
81
|
+
export class StatusReaction {
|
|
82
|
+
#slack;
|
|
83
|
+
#channel;
|
|
84
|
+
#rootTs;
|
|
85
|
+
#log;
|
|
86
|
+
#lock = new Mutex();
|
|
87
|
+
#current = null;
|
|
88
|
+
// What `show` or `clear` was asked for last, set before any call: it differs from
|
|
89
|
+
// `#current` while a change is on its way, and after one that never finished.
|
|
90
|
+
#wanted = null;
|
|
91
|
+
constructor(slack, options) {
|
|
92
|
+
this.#slack = slack;
|
|
93
|
+
this.#channel = options.channel;
|
|
94
|
+
this.#rootTs = options.rootTs;
|
|
95
|
+
this.#log = options.logger ?? logger;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The reaction this instance last showed successfully; `null` before that, and after a
|
|
99
|
+
* change that was aborted on its way.
|
|
100
|
+
*/
|
|
101
|
+
get current() {
|
|
102
|
+
return this.#current;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Shows `state`. False when the root is there and does not carry it: after an add that
|
|
106
|
+
* failed, and once no instance calls Slack for reactions. A root Slack no longer has (`GONE`)
|
|
107
|
+
* has nothing left to show, and answers true. With a `signal`, rejects with its reason if it
|
|
108
|
+
* aborts (see the file header).
|
|
109
|
+
*/
|
|
110
|
+
async show(state, signal) {
|
|
111
|
+
if (missingScope)
|
|
112
|
+
return false;
|
|
113
|
+
this.#wanted = state;
|
|
114
|
+
const release = await this.#lock.acquire(signal);
|
|
115
|
+
try {
|
|
116
|
+
if (state === this.#current)
|
|
117
|
+
return true;
|
|
118
|
+
const previous = this.#current;
|
|
119
|
+
try {
|
|
120
|
+
const added = await this.#add(state);
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
// `#current` stays as it was: the next `show` retries the add
|
|
123
|
+
if (added !== "added")
|
|
124
|
+
return added === "gone";
|
|
125
|
+
if (previous !== null) {
|
|
126
|
+
await this.#remove(previous);
|
|
127
|
+
signal?.throwIfAborted();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// A fresh instance: an earlier session's reaction may still sit on this
|
|
131
|
+
// root (an idle close's DONE, a restart's ERROR). Strip every other name so
|
|
132
|
+
// the root carries exactly this one.
|
|
133
|
+
for (const other of Object.values(Status)) {
|
|
134
|
+
if (other !== state) {
|
|
135
|
+
await this.#remove(other);
|
|
136
|
+
signal?.throwIfAborted();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Issue #104: the root may carry the new name, the previous one or both. Read
|
|
143
|
+
// as a fresh instance from here, so the next change strips every other name. Nothing
|
|
144
|
+
// else throws in here: `#add` and `#remove` swallow every Slack failure.
|
|
145
|
+
this.#current = null;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
this.#current = state;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
release();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Remove the current reaction, leaving the root bare: for a caller with nothing to
|
|
157
|
+
* revert to (the same-folder hold's Cancel on a session that had shown no reaction yet).
|
|
158
|
+
*/
|
|
159
|
+
async clear() {
|
|
160
|
+
this.#wanted = null;
|
|
161
|
+
const release = await this.#lock.acquire();
|
|
162
|
+
try {
|
|
163
|
+
if (this.#current === null)
|
|
164
|
+
return;
|
|
165
|
+
await this.#remove(this.#current);
|
|
166
|
+
this.#current = null;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
release();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Bring the root to the state asked for last, making no call when it already shows it.
|
|
174
|
+
* For the caller that aborted a change in the middle (a session's close, issue #104), which is
|
|
175
|
+
* the only one left to finish it.
|
|
176
|
+
*/
|
|
177
|
+
async settle() {
|
|
178
|
+
if (this.#wanted === null)
|
|
179
|
+
await this.clear();
|
|
180
|
+
else
|
|
181
|
+
await this.show(this.#wanted);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Whether the root now carries `state` (on success or `already_reacted`), is no longer in
|
|
185
|
+
* Slack, or neither.
|
|
186
|
+
*/
|
|
187
|
+
async #add(state) {
|
|
188
|
+
try {
|
|
189
|
+
await this.#slack.reactions.add({
|
|
190
|
+
channel: this.#channel,
|
|
191
|
+
timestamp: this.#rootTs,
|
|
192
|
+
name: state,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const code = describe(error);
|
|
197
|
+
if (code === "already_reacted")
|
|
198
|
+
return "added";
|
|
199
|
+
if (code === "missing_scope")
|
|
200
|
+
this.#noteMissingScope();
|
|
201
|
+
else {
|
|
202
|
+
this.#log.warning(`reactions.add failed on ${this.#channel}/${this.#rootTs}: ${code}`);
|
|
203
|
+
}
|
|
204
|
+
return gone(error) ? "gone" : "failed";
|
|
205
|
+
}
|
|
206
|
+
return "added";
|
|
207
|
+
}
|
|
208
|
+
async #remove(state) {
|
|
209
|
+
try {
|
|
210
|
+
await this.#slack.reactions.remove({
|
|
211
|
+
channel: this.#channel,
|
|
212
|
+
timestamp: this.#rootTs,
|
|
213
|
+
name: state,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const code = describe(error);
|
|
218
|
+
if (code === "no_reaction")
|
|
219
|
+
return;
|
|
220
|
+
if (code === "missing_scope")
|
|
221
|
+
this.#noteMissingScope();
|
|
222
|
+
else {
|
|
223
|
+
this.#log.warning(`reactions.remove failed on ${this.#channel}/${this.#rootTs}: ${code}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
#noteMissingScope() {
|
|
228
|
+
if (!missingScope) {
|
|
229
|
+
this.#log.warning(`reactions.add/remove failed on ${this.#channel}/${this.#rootTs}: missing_scope ` +
|
|
230
|
+
"(reactions:write); the app needs reinstalling with the current scopes; no more " +
|
|
231
|
+
"reactions will be attempted this run");
|
|
232
|
+
}
|
|
233
|
+
missingScope = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Slack's status line under a thread's last message, saying what `show` was last given:
|
|
238
|
+
* `Working…` as the sign that a prompt was received and that its turn runs, where a reply
|
|
239
|
+
* that has written nothing yet, or nothing for a while, gives none (issue #83), then what
|
|
240
|
+
* still runs once the turn has ended (`1 shell still running`, issue #95). A state that
|
|
241
|
+
* changes belongs here and not in a reply: a stream cannot change what it was told.
|
|
242
|
+
* It notifies nobody (measured 2026-09-28) and shows on desktop and on iOS once it carries
|
|
243
|
+
* `loading_messages` (measured 2026-10-02). `show` and `wrote` never wait on Slack: one task
|
|
244
|
+
* of this instance's own makes the calls, in order, so two quick changes end on the last one.
|
|
245
|
+
* It sets the status at once, again within THREAD_STATUS_AFTER_WRITE_SECONDS of a `wrote`,
|
|
246
|
+
* and every THREAD_STATUS_REFRESH_SECONDS; `show("")` clears it, only if it was set, and a
|
|
247
|
+
* clearing call that fails is tried once more. A
|
|
248
|
+
* failure is logged (channel, ts and the error code only, once per code in a row) and
|
|
249
|
+
* swallowed, since a status line must never break a turn. A refusal no retry can change
|
|
250
|
+
* (`THREAD_STATUS_REFUSED`: the token cannot call the method) stops every instance from
|
|
251
|
+
* calling Slack for the rest of the run, as `StatusReaction` does for `missing_scope`.
|
|
252
|
+
*/
|
|
253
|
+
export class ThreadStatus {
|
|
254
|
+
#slack;
|
|
255
|
+
#channel;
|
|
256
|
+
#threadTs;
|
|
257
|
+
#clock;
|
|
258
|
+
#refresh;
|
|
259
|
+
#afterWrite;
|
|
260
|
+
#log;
|
|
261
|
+
#text = ""; // what it says; empty: nothing is shown
|
|
262
|
+
#fallback = texts.THREAD_WORKING_STATUS;
|
|
263
|
+
#shown = false; // whether Slack was last asked to show it
|
|
264
|
+
#due = 0; // when it is next set, by the clock
|
|
265
|
+
#woken = false; // asked for again since the task last looked
|
|
266
|
+
#waiter = null; // the task's nap, when it is in one
|
|
267
|
+
#keeper = null;
|
|
268
|
+
#failed = null;
|
|
269
|
+
constructor(slack, options) {
|
|
270
|
+
this.#slack = slack;
|
|
271
|
+
this.#channel = options.channel;
|
|
272
|
+
this.#threadTs = options.threadTs;
|
|
273
|
+
this.#clock = options.clock;
|
|
274
|
+
this.#refresh = options.refresh ?? THREAD_STATUS_REFRESH_SECONDS;
|
|
275
|
+
this.#afterWrite = options.afterWrite ?? THREAD_STATUS_AFTER_WRITE_SECONDS;
|
|
276
|
+
this.#log = options.logger ?? logger;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether Slack has said this app's token cannot set a thread status at all: what a
|
|
280
|
+
* caller with something the owner must be told reads before it falls back on a message.
|
|
281
|
+
*/
|
|
282
|
+
static refused() {
|
|
283
|
+
return refusedStatus;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Say `text` from now on, or with an empty one stop showing the status. The same text
|
|
287
|
+
* again is a no-op. `fallback` says the same after the app's name, for a client that
|
|
288
|
+
* draws `<app name> <status>` instead of the loading message.
|
|
289
|
+
*/
|
|
290
|
+
show(text, fallback = texts.THREAD_WORKING_STATUS) {
|
|
291
|
+
if (text === this.#text)
|
|
292
|
+
return;
|
|
293
|
+
this.#text = text;
|
|
294
|
+
this.#fallback = fallback;
|
|
295
|
+
this.#due = this.#clock.time();
|
|
296
|
+
this.#kick();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Something of the app's was written in this thread, which may have cleared the
|
|
300
|
+
* status: it is set again within THREAD_STATUS_AFTER_WRITE_SECONDS, never later than it
|
|
301
|
+
* was already due (writes that keep coming must not put off the refresh).
|
|
302
|
+
*/
|
|
303
|
+
wrote() {
|
|
304
|
+
if (this.#text) {
|
|
305
|
+
this.#due = Math.min(this.#due, this.#clock.time() + this.#afterWrite);
|
|
306
|
+
this.#kick();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** Stop for good: the status is cleared if it shows, and nothing is left running. */
|
|
310
|
+
async close() {
|
|
311
|
+
this.#text = "";
|
|
312
|
+
const keeper = this.#keeper;
|
|
313
|
+
this.#keeper = null;
|
|
314
|
+
if (keeper !== null && !keeper.done) {
|
|
315
|
+
keeper.stop.abort();
|
|
316
|
+
await keeper.finished.catch(() => undefined);
|
|
317
|
+
}
|
|
318
|
+
if (this.#shown) {
|
|
319
|
+
this.#shown = false;
|
|
320
|
+
await this.#set(false);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
#kick() {
|
|
324
|
+
this.#woken = true;
|
|
325
|
+
this.#waiter?.();
|
|
326
|
+
if (this.#keeper === null || this.#keeper.done) {
|
|
327
|
+
const stop = new AbortController();
|
|
328
|
+
const keeper = { stop, done: false, finished: Promise.resolve() };
|
|
329
|
+
keeper.finished = this.#keep(keeper);
|
|
330
|
+
this.#keeper = keeper;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Waits `seconds` on the clock, until `#kick` wakes it, or until the task is stopped. */
|
|
334
|
+
#nap(seconds, signal) {
|
|
335
|
+
if (this.#woken || signal.aborted)
|
|
336
|
+
return Promise.resolve();
|
|
337
|
+
const napping = new AbortController();
|
|
338
|
+
const until = AbortSignal.any([signal, napping.signal]);
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
let over = false;
|
|
341
|
+
const wake = () => {
|
|
342
|
+
if (over)
|
|
343
|
+
return;
|
|
344
|
+
over = true;
|
|
345
|
+
this.#waiter = null;
|
|
346
|
+
napping.abort(); // lets the clock forget the sleep
|
|
347
|
+
resolve();
|
|
348
|
+
};
|
|
349
|
+
this.#waiter = wake;
|
|
350
|
+
this.#clock.sleep(seconds, until).then(wake, wake);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async #keep(keeper) {
|
|
354
|
+
const signal = keeper.stop.signal;
|
|
355
|
+
let retried = false;
|
|
356
|
+
try {
|
|
357
|
+
await Promise.resolve(); // the caller's code finishes first
|
|
358
|
+
while (true) {
|
|
359
|
+
this.#woken = false;
|
|
360
|
+
if (signal.aborted)
|
|
361
|
+
return;
|
|
362
|
+
if (!this.#text) {
|
|
363
|
+
if (this.#shown) {
|
|
364
|
+
// Cleared only once the call is back and went through: a `close` that
|
|
365
|
+
// stops it meanwhile, or that follows a failed one, still reads the
|
|
366
|
+
// status as shown and clears it itself.
|
|
367
|
+
if (await this.#set(false)) {
|
|
368
|
+
this.#shown = false;
|
|
369
|
+
}
|
|
370
|
+
else if (!retried) {
|
|
371
|
+
// Once more after a moment: a status left standing says a session
|
|
372
|
+
// works that does not, until Slack removes it by itself.
|
|
373
|
+
retried = true;
|
|
374
|
+
await this.#nap(this.#afterWrite, signal);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (this.#woken)
|
|
379
|
+
continue; // asked for again while that call was out
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
retried = false;
|
|
383
|
+
const delay = this.#due - this.#clock.time();
|
|
384
|
+
if (delay <= 0) {
|
|
385
|
+
// Before the call, so a `wrote` that arrives while it is out still moves it.
|
|
386
|
+
this.#due = this.#clock.time() + this.#refresh;
|
|
387
|
+
this.#shown = true;
|
|
388
|
+
await this.#set(true);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
await this.#nap(delay, signal);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
keeper.done = true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/** Tell Slack; whether it went through, or nothing is left to try (`refusedStatus`). */
|
|
399
|
+
async #set(on) {
|
|
400
|
+
if (refusedStatus)
|
|
401
|
+
return true;
|
|
402
|
+
try {
|
|
403
|
+
if (on) {
|
|
404
|
+
await this.#slack.assistant.threads.setStatus({
|
|
405
|
+
channel_id: this.#channel,
|
|
406
|
+
thread_ts: this.#threadTs,
|
|
407
|
+
status: this.#fallback,
|
|
408
|
+
loading_messages: [this.#text],
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
// An empty status clears it (the method's reference).
|
|
413
|
+
await this.#slack.assistant.threads.setStatus({
|
|
414
|
+
channel_id: this.#channel,
|
|
415
|
+
thread_ts: this.#threadTs,
|
|
416
|
+
status: "",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
const code = describe(error);
|
|
422
|
+
if (code !== this.#failed) {
|
|
423
|
+
this.#log.warning(`assistant.threads.setStatus failed on ${this.#channel}/${this.#threadTs}: ${code}`);
|
|
424
|
+
}
|
|
425
|
+
this.#failed = code;
|
|
426
|
+
if (THREAD_STATUS_REFUSED.includes(code))
|
|
427
|
+
refusedStatus = true;
|
|
428
|
+
return refusedStatus;
|
|
429
|
+
}
|
|
430
|
+
this.#failed = null;
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Task as SessionTask } from "../../../core/port.js";
|
|
2
|
+
/**
|
|
3
|
+
* The task of the provider's passes: the sessions' `Task`, started when it is made, where a
|
|
4
|
+
* session's own starts on the next turn of the event loop. The rule it serves is the header of
|
|
5
|
+
* `sinks.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export class Task extends SessionTask {
|
|
8
|
+
constructor(run) {
|
|
9
|
+
super(run, { immediate: true });
|
|
10
|
+
}
|
|
11
|
+
}
|