allaboard 0.0.0-stage → 0.5.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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -2
  3. package/dist/agent/claude/backend.js +124 -0
  4. package/dist/agent/claude/capabilities.js +30 -0
  5. package/dist/agent/claude/chrome.js +42 -0
  6. package/dist/agent/claude/hooks.js +37 -0
  7. package/dist/agent/claude/info.js +61 -0
  8. package/dist/agent/claude/listing.js +230 -0
  9. package/dist/agent/claude/module.js +82 -0
  10. package/dist/agent/claude/prompt.js +24 -0
  11. package/dist/agent/claude/record.js +9 -0
  12. package/dist/agent/claude/requests.js +122 -0
  13. package/dist/agent/claude/session.js +441 -0
  14. package/dist/agent/claude/translate.js +581 -0
  15. package/dist/agent/claude/trust.js +576 -0
  16. package/dist/agent/claude/usage-probe.js +103 -0
  17. package/dist/agent/claude/usage.js +110 -0
  18. package/dist/agent/claude/wire.js +22 -0
  19. package/dist/agent/seam.js +21 -0
  20. package/dist/chat/seam.js +10 -0
  21. package/dist/chat/slack/app/answers.js +209 -0
  22. package/dist/chat/slack/app/app.js +262 -0
  23. package/dist/chat/slack/app/clip.js +191 -0
  24. package/dist/chat/slack/app/guards.js +162 -0
  25. package/dist/chat/slack/app/home-controls.js +91 -0
  26. package/dist/chat/slack/app/messages.js +181 -0
  27. package/dist/chat/slack/app/open.js +392 -0
  28. package/dist/chat/slack/app/order.js +37 -0
  29. package/dist/chat/slack/app/requests.js +482 -0
  30. package/dist/chat/slack/app/resume-bind.js +71 -0
  31. package/dist/chat/slack/app/wire.js +83 -0
  32. package/dist/chat/slack/attachments.js +348 -0
  33. package/dist/chat/slack/bind.js +67 -0
  34. package/dist/chat/slack/channel.js +67 -0
  35. package/dist/chat/slack/channels.js +43 -0
  36. package/dist/chat/slack/delete.js +278 -0
  37. package/dist/chat/slack/footer.js +43 -0
  38. package/dist/chat/slack/hold.js +37 -0
  39. package/dist/chat/slack/home.js +1069 -0
  40. package/dist/chat/slack/module.js +174 -0
  41. package/dist/chat/slack/notice.js +64 -0
  42. package/dist/chat/slack/openfile/modal.js +362 -0
  43. package/dist/chat/slack/openfile/read.js +29 -0
  44. package/dist/chat/slack/quiet-logger.js +52 -0
  45. package/dist/chat/slack/repair.js +123 -0
  46. package/dist/chat/slack/reply/blocks.js +372 -0
  47. package/dist/chat/slack/reply/chars.js +59 -0
  48. package/dist/chat/slack/reply/clients.js +171 -0
  49. package/dist/chat/slack/reply/errors.js +125 -0
  50. package/dist/chat/slack/reply/escape.js +25 -0
  51. package/dist/chat/slack/reply/limiter.js +86 -0
  52. package/dist/chat/slack/reply/markdown.js +210 -0
  53. package/dist/chat/slack/reply/places.js +262 -0
  54. package/dist/chat/slack/reply/sinks.js +2403 -0
  55. package/dist/chat/slack/reply/status.js +433 -0
  56. package/dist/chat/slack/reply/tasks.js +11 -0
  57. package/dist/chat/slack/requests.js +552 -0
  58. package/dist/chat/slack/resume.js +127 -0
  59. package/dist/chat/slack/setup.js +114 -0
  60. package/dist/chat/slack/texts.js +185 -0
  61. package/dist/chat/slack/thread.js +537 -0
  62. package/dist/chat/slack/update.js +71 -0
  63. package/dist/chat/slack/voice.js +84 -0
  64. package/dist/cli.js +326 -0
  65. package/dist/clock.js +21 -0
  66. package/dist/core/cleanup.js +145 -0
  67. package/dist/core/commands.js +172 -0
  68. package/dist/core/config.js +174 -0
  69. package/dist/core/files/changed.js +125 -0
  70. package/dist/core/files/listing.js +382 -0
  71. package/dist/core/files/paths.js +9 -0
  72. package/dist/core/files/read.js +191 -0
  73. package/dist/core/files/waiting.js +34 -0
  74. package/dist/core/flows/answers.js +133 -0
  75. package/dist/core/flows/ask-owner.js +95 -0
  76. package/dist/core/flows/asking.js +14 -0
  77. package/dist/core/flows/bind.js +95 -0
  78. package/dist/core/flows/contract.js +1 -0
  79. package/dist/core/flows/flows.js +35 -0
  80. package/dist/core/flows/hold.js +22 -0
  81. package/dist/core/flows/inbound.js +24 -0
  82. package/dist/core/flows/lists.js +21 -0
  83. package/dist/core/flows/messages.js +293 -0
  84. package/dist/core/flows/notices.js +51 -0
  85. package/dist/core/flows/repair.js +106 -0
  86. package/dist/core/flows/resume.js +163 -0
  87. package/dist/core/flows/setup.js +130 -0
  88. package/dist/core/flows/status.js +98 -0
  89. package/dist/core/flows/talk.js +207 -0
  90. package/dist/core/flows/update.js +370 -0
  91. package/dist/core/flows/words.js +171 -0
  92. package/dist/core/folders.js +93 -0
  93. package/dist/core/footer.js +313 -0
  94. package/dist/core/guide.js +50 -0
  95. package/dist/core/hold.js +126 -0
  96. package/dist/core/instance.js +46 -0
  97. package/dist/core/lock.js +412 -0
  98. package/dist/core/models.js +10 -0
  99. package/dist/core/paths.js +75 -0
  100. package/dist/core/port/file-flows.js +1 -0
  101. package/dist/core/port/home.js +1 -0
  102. package/dist/core/port/requests.js +1 -0
  103. package/dist/core/port/threads.js +1 -0
  104. package/dist/core/port/updates.js +1 -0
  105. package/dist/core/port.js +17 -0
  106. package/dist/core/reply/fold.js +175 -0
  107. package/dist/core/reply/previews.js +258 -0
  108. package/dist/core/reply/renderer.js +846 -0
  109. package/dist/core/reply/tail.js +24 -0
  110. package/dist/core/reply/words.js +32 -0
  111. package/dist/core/requests.js +158 -0
  112. package/dist/core/sessions/constants.js +61 -0
  113. package/dist/core/sessions/deps.js +1 -0
  114. package/dist/core/sessions/directory.js +86 -0
  115. package/dist/core/sessions/errors.js +79 -0
  116. package/dist/core/sessions/manager.js +378 -0
  117. package/dist/core/sessions/session.js +3297 -0
  118. package/dist/core/sessions/tasks.js +266 -0
  119. package/dist/core/sessions/turn.js +45 -0
  120. package/dist/core/setup.js +28 -0
  121. package/dist/core/state.js +724 -0
  122. package/dist/core/texts.js +451 -0
  123. package/dist/core/updates.js +332 -0
  124. package/dist/daemon.js +566 -0
  125. package/dist/host/npm.js +134 -0
  126. package/dist/host/program.js +49 -0
  127. package/dist/host/run.js +52 -0
  128. package/dist/host/service/launchd.js +385 -0
  129. package/dist/host/service/listing.js +42 -0
  130. package/dist/host/service/seam.js +1 -0
  131. package/dist/log.js +37 -0
  132. package/dist/main.js +29 -0
  133. package/dist/node-version.js +40 -0
  134. package/dist/registry.js +12 -0
  135. package/dist/semver.js +53 -0
  136. package/dist/text.js +89 -0
  137. package/dist/updates-host.js +113 -0
  138. package/dist/values.js +30 -0
  139. package/dist/version.js +54 -0
  140. package/package.json +32 -4
  141. 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
+ }