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,378 @@
|
|
|
1
|
+
import { errorName } from "../../values.js";
|
|
2
|
+
import { bindableFolders } from "../folders.js";
|
|
3
|
+
import * as texts from "../texts.js";
|
|
4
|
+
import { DRAIN_POLL_SECONDS } from "./constants.js";
|
|
5
|
+
import { checkDirectory, repositoryBy, trustedBy } from "./directory.js";
|
|
6
|
+
import { DirectoryUnavailable, SessionClosed } from "./errors.js";
|
|
7
|
+
import { logger, ThreadSession } from "./session.js";
|
|
8
|
+
function keyOf(channelId, threadId) {
|
|
9
|
+
return `${channelId}\n${threadId}`;
|
|
10
|
+
}
|
|
11
|
+
export class SessionManager {
|
|
12
|
+
deps;
|
|
13
|
+
names;
|
|
14
|
+
sessions = new Map();
|
|
15
|
+
/** The threads being deleted (`release`): no session is handed out or built in one. */
|
|
16
|
+
heldThreads = new Set();
|
|
17
|
+
/**
|
|
18
|
+
* Set at the top of `closeAll`: a listener still in flight when a stop ends must not build a
|
|
19
|
+
* session nobody will close, nor write `state.json` after the lock was released (Node's
|
|
20
|
+
* listeners keep running after a stop).
|
|
21
|
+
*/
|
|
22
|
+
closed = false;
|
|
23
|
+
draining = false;
|
|
24
|
+
/** Whether an install is replacing allaboard's own copy: no session starts an agent process. */
|
|
25
|
+
startsBlocked = false;
|
|
26
|
+
constructor(deps) {
|
|
27
|
+
this.deps = deps;
|
|
28
|
+
this.names = texts.namesOf(deps.agent, deps.chat);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A top-level owner message: creates the thread's entry, in the channel's current folder,
|
|
32
|
+
* and its live session. Null when the channel is unbound; `SessionClosed` once a stop closed
|
|
33
|
+
* the sessions, before anything is written.
|
|
34
|
+
*/
|
|
35
|
+
open(channelId, threadId) {
|
|
36
|
+
this.refuseWhenClosed();
|
|
37
|
+
if (this.deps.state.channel(channelId) === null)
|
|
38
|
+
return null;
|
|
39
|
+
const thread = this.deps.state.openThread(channelId, threadId);
|
|
40
|
+
return this.session(channelId, threadId, thread.directory);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The repository holding `directory` when the daemon's git may run in it: the footer's own
|
|
44
|
+
* lookup (`repositoryBy`), so what runs git for `!open` is what runs it for the footer.
|
|
45
|
+
*/
|
|
46
|
+
repository(directory) {
|
|
47
|
+
return repositoryBy(this.deps.agent)(directory);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Something of the app's was posted in this thread outside its session (the answer to a
|
|
51
|
+
* word, a notice): the live session's activity line, which that post cleared, is set again.
|
|
52
|
+
* Nothing for a thread with no live session: it shows none.
|
|
53
|
+
*/
|
|
54
|
+
wrote(channelId, threadId) {
|
|
55
|
+
this.sessions.get(keyOf(channelId, threadId))?.threadWritten();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The thread as a flow answers in it: the live session's own, so that what is posted there
|
|
59
|
+
* sets its activity line again, or the chat's for a thread with no live session.
|
|
60
|
+
*/
|
|
61
|
+
chatOf(channelId, threadId) {
|
|
62
|
+
return (this.sessions.get(keyOf(channelId, threadId))?.threadChat ??
|
|
63
|
+
this.deps.chat.thread({ channel: channelId, thread: threadId }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The live session of a thread, or one rebuilt from its stored entry (after a restart, the
|
|
67
|
+
* idle close, or a resume whose session turned out gone); null when the thread is not a
|
|
68
|
+
* session. A closed session (`close()` has started: shutdown, an idle close, or the
|
|
69
|
+
* SessionGone branch of `ensureConnected`) is never handed back here: it is discarded and
|
|
70
|
+
* replaced, so a message never reaches a client that is going, or gone. The session handed
|
|
71
|
+
* back has just been touched: its idle-close timer cannot fire before the caller's own next
|
|
72
|
+
* await, however slow (a download, a slow call to the chat). Null for every thread once a stop
|
|
73
|
+
* closed the sessions.
|
|
74
|
+
*/
|
|
75
|
+
get(channelId, threadId) {
|
|
76
|
+
if (this.closed)
|
|
77
|
+
return null;
|
|
78
|
+
const thread = this.deps.state.thread(channelId, threadId);
|
|
79
|
+
if (thread === null || this.heldThreads.has(keyOf(channelId, threadId)))
|
|
80
|
+
return null;
|
|
81
|
+
return this.session(channelId, threadId, thread.directory);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether `get` would give the thread a session. Asking does what `get` does: it touches the
|
|
85
|
+
* session, or rebuilds one from `state.json`, so a clip is told what a message would be.
|
|
86
|
+
*/
|
|
87
|
+
live(channelId, threadId) {
|
|
88
|
+
return this.get(channelId, threadId) !== null;
|
|
89
|
+
}
|
|
90
|
+
refuseWhenClosed() {
|
|
91
|
+
if (this.closed)
|
|
92
|
+
throw new SessionClosed();
|
|
93
|
+
}
|
|
94
|
+
session(channelId, threadId, directory) {
|
|
95
|
+
const key = keyOf(channelId, threadId);
|
|
96
|
+
let session = this.sessions.get(key) ?? null;
|
|
97
|
+
let predecessor = null;
|
|
98
|
+
if (session?.closed) {
|
|
99
|
+
// Discarded as soon as closing has started, not once it finishes: a message must never
|
|
100
|
+
// reach an object mid-teardown (every operation on it rejects with SessionClosed). The
|
|
101
|
+
// replacement waits on the old session's own `doneClosing` before it connects, so a
|
|
102
|
+
// rebuild never resumes the same id while the old process is still exiting.
|
|
103
|
+
this.sessions.delete(key);
|
|
104
|
+
predecessor = session.doneClosing;
|
|
105
|
+
session = null;
|
|
106
|
+
}
|
|
107
|
+
if (session === null) {
|
|
108
|
+
const built = new ThreadSession(channelId, threadId, directory, this.deps, { predecessor });
|
|
109
|
+
built.draining = this.draining;
|
|
110
|
+
built.startsBlocked = this.startsBlocked;
|
|
111
|
+
built.onClosed = () => this.evictIfCurrent(key, built);
|
|
112
|
+
this.sessions.set(key, built);
|
|
113
|
+
session = built;
|
|
114
|
+
}
|
|
115
|
+
// (sync) About to be handed to a caller: never let the idle timer close it out from under
|
|
116
|
+
// whatever slow step (a download, a call to the chat) the caller does before its own
|
|
117
|
+
// submit.
|
|
118
|
+
session.touch();
|
|
119
|
+
return session;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Drop a fully-closed session from the live map, but only if nothing has already replaced it
|
|
123
|
+
* at this key (keeps a long-idle thread's dead object from piling up).
|
|
124
|
+
*/
|
|
125
|
+
evictIfCurrent(key, session) {
|
|
126
|
+
if (this.sessions.get(key) === session)
|
|
127
|
+
this.sessions.delete(key);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Every thread with a session object that is not closed: what the cleanup of `state.json`
|
|
131
|
+
* must not remove under a running session.
|
|
132
|
+
*/
|
|
133
|
+
liveThreads() {
|
|
134
|
+
return [...this.sessions.values()]
|
|
135
|
+
.filter((session) => !session.closed)
|
|
136
|
+
.map((session) => ({ channel: session.channelId, thread: session.threadId }));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The live sessions of a channel, across its threads; a closed one (the idle close, or a
|
|
140
|
+
* gone resume) is left out even before the next lookup evicts it.
|
|
141
|
+
*/
|
|
142
|
+
sessionsOf(channelId) {
|
|
143
|
+
return [...this.sessions.values()].filter((session) => session.channelId === channelId && !session.closed);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* While a stop waits: each thread that holds it, of any channel, with what it waits for
|
|
147
|
+
* there (`ThreadSession.restartHold`). Empty when no stop is under way.
|
|
148
|
+
*/
|
|
149
|
+
restartHolds() {
|
|
150
|
+
if (!this.draining)
|
|
151
|
+
return [];
|
|
152
|
+
return [...this.sessions.values()]
|
|
153
|
+
.filter((session) => !session.closed)
|
|
154
|
+
.map((session) => [session, session.restartHold])
|
|
155
|
+
.filter(([, hold]) => hold !== "");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A live session of any channel, other than `besides`, whose folder resolves to the same one
|
|
159
|
+
* as `besides`'s own and is not idle (a background task counts as working, same as `idle`
|
|
160
|
+
* already treats it). Each session's own `resolvedDirectory` is kept from its construction:
|
|
161
|
+
* comparing it avoids resolving a path per live session on every message. The first one
|
|
162
|
+
* found is enough: the question links to it.
|
|
163
|
+
*/
|
|
164
|
+
workingIn(options) {
|
|
165
|
+
const { besides } = options;
|
|
166
|
+
const resolved = besides.resolvedDirectory;
|
|
167
|
+
for (const session of this.sessions.values()) {
|
|
168
|
+
if (session !== besides &&
|
|
169
|
+
!session.closed &&
|
|
170
|
+
!session.idle &&
|
|
171
|
+
session.resolvedDirectory === resolved) {
|
|
172
|
+
return session;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Bind the channel to `directory`, for the next thread it opens; false, and nothing changed,
|
|
179
|
+
* while any live session of the channel is not idle. A thread already open keeps the folder
|
|
180
|
+
* it started in. `SessionClosed` once a stop closed the sessions.
|
|
181
|
+
*/
|
|
182
|
+
async bind(channelId, directory) {
|
|
183
|
+
this.refuseWhenClosed();
|
|
184
|
+
if (this.sessionsOf(channelId).some((session) => !session.idle))
|
|
185
|
+
return false;
|
|
186
|
+
this.deps.state.bind(channelId, directory);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Close the thread's live session so the thread can be deleted, and wait for its teardown.
|
|
191
|
+
* False, with nothing closed, when the session is not idle or waits for the owner: the same
|
|
192
|
+
* test the idle close makes, so the close is as silent. True holds the thread until `free`:
|
|
193
|
+
* `get` answers null for it meanwhile and `held` says so, so a message sent in a thread that
|
|
194
|
+
* is being deleted is told it holds no session. False once a stop closed the sessions: no
|
|
195
|
+
* thread is held for a delete the stop would cut short.
|
|
196
|
+
*/
|
|
197
|
+
async release(channelId, threadId) {
|
|
198
|
+
if (this.closed)
|
|
199
|
+
return false;
|
|
200
|
+
const key = keyOf(channelId, threadId);
|
|
201
|
+
const session = this.sessions.get(key);
|
|
202
|
+
const inUse = session !== undefined && !session.closed && (session.waitingForOwner || !session.idle);
|
|
203
|
+
if (inUse)
|
|
204
|
+
return false;
|
|
205
|
+
// (sync) Held before the first await: a message that arrives while the session closes, or
|
|
206
|
+
// while the thread's messages are deleted, must not build a session in it.
|
|
207
|
+
this.heldThreads.add(key);
|
|
208
|
+
if (session !== undefined) {
|
|
209
|
+
if (!session.closed)
|
|
210
|
+
await session.close(texts.ENDED_IDLE);
|
|
211
|
+
await session.doneClosing.wait();
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
/** Whether the thread is being deleted: it still has its entry, and takes no session. */
|
|
216
|
+
held(channelId, threadId) {
|
|
217
|
+
return this.heldThreads.has(keyOf(channelId, threadId));
|
|
218
|
+
}
|
|
219
|
+
/** End the hold `release` put on a thread: its delete ended, either way. */
|
|
220
|
+
free(channelId, threadId) {
|
|
221
|
+
this.heldThreads.delete(keyOf(channelId, threadId));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* `stop()` on every live session of the channel; true if any stopped something. Null when
|
|
225
|
+
* nothing did except cancel a hold: the caller adds no further notice of its own then, the
|
|
226
|
+
* same as `ThreadSession.stop`'s own null, since the held thread already got `Not sent.`.
|
|
227
|
+
*/
|
|
228
|
+
async stopChannel(channelId) {
|
|
229
|
+
let stopped = false;
|
|
230
|
+
let heldOnly = false;
|
|
231
|
+
for (const session of this.sessionsOf(channelId)) {
|
|
232
|
+
const result = await session.stop();
|
|
233
|
+
if (result)
|
|
234
|
+
stopped = true;
|
|
235
|
+
else if (result === null)
|
|
236
|
+
heldOnly = true;
|
|
237
|
+
}
|
|
238
|
+
if (stopped)
|
|
239
|
+
return true;
|
|
240
|
+
return heldOnly ? null : false;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* `!resume`: opens a thread at `threadId` (the `!resume` message's own ts) in the channel's
|
|
244
|
+
* folder, already on `sessionId`. Null when the channel is unbound; `SessionClosed` once a
|
|
245
|
+
* stop closed the sessions.
|
|
246
|
+
*/
|
|
247
|
+
async resume(channelId, threadId, sessionId) {
|
|
248
|
+
this.refuseWhenClosed();
|
|
249
|
+
if (this.deps.state.channel(channelId) === null)
|
|
250
|
+
return null;
|
|
251
|
+
const thread = this.deps.state.openThread(channelId, threadId, sessionId);
|
|
252
|
+
return this.session(channelId, threadId, thread.directory);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Every session of `directory`, the one the caller read for its channel; `dated`, by their
|
|
256
|
+
* last message, newest first, as the terminal's picker shows them. None for an agent that
|
|
257
|
+
* lists none (`sessionListing`).
|
|
258
|
+
*/
|
|
259
|
+
async sessionsIn(directory, options = {}) {
|
|
260
|
+
if (!this.deps.agent.capabilities.sessionListing)
|
|
261
|
+
return [];
|
|
262
|
+
const found = await this.deps.agent.listSessions(directory);
|
|
263
|
+
return options.dated ? this.dated(directory, found) : found;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* `found`, sessions of `directory`, by their last message, newest first. Dating reads files,
|
|
267
|
+
* so a caller that shows only some of a folder's sessions passes those alone.
|
|
268
|
+
*/
|
|
269
|
+
async dated(directory, found) {
|
|
270
|
+
if (!this.deps.agent.capabilities.sessionListing)
|
|
271
|
+
return [];
|
|
272
|
+
return this.deps.agent.datedSessions(directory, found);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* What would keep a session from starting in `directory`, by the checks a start makes; null
|
|
276
|
+
* when nothing would.
|
|
277
|
+
*/
|
|
278
|
+
async unavailable(directory) {
|
|
279
|
+
try {
|
|
280
|
+
await checkDirectory(directory, trustedBy(this.deps.agent), this.names);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (error instanceof DirectoryUnavailable)
|
|
284
|
+
return error;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
/** The folders under `root` where a session can start, as `bindableFolders` finds them. */
|
|
290
|
+
foldersIn(root) {
|
|
291
|
+
return bindableFolders(root, trustedBy(this.deps.agent));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Blocks every start of an agent process, in the sessions now and in those built later, until
|
|
295
|
+
* the function returned is called: an install replaces the copy the sessions run from
|
|
296
|
+
* (issue #361). A session already connected goes on. A start under way is not recalled.
|
|
297
|
+
*/
|
|
298
|
+
blockStarts() {
|
|
299
|
+
this.startsBlocked = true;
|
|
300
|
+
for (const session of this.sessions.values())
|
|
301
|
+
session.startsBlocked = true;
|
|
302
|
+
return () => {
|
|
303
|
+
this.startsBlocked = false;
|
|
304
|
+
for (const session of this.sessions.values())
|
|
305
|
+
session.startsBlocked = false;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Whether an agent process may not start now: this daemon's install blocks it, or another
|
|
310
|
+
* process of the machine is updating allaboard (`SessionDeps.updating`).
|
|
311
|
+
*/
|
|
312
|
+
async refusesStarts() {
|
|
313
|
+
return this.startsBlocked || ((await this.deps.updating?.()) ?? false);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Let the turns already sent finish and send no other, then return: when every channel is
|
|
317
|
+
* idle, or when `cutShort` aborts. Idle includes the background commands and agents, which
|
|
318
|
+
* die with the agent's process, and the turn the agent starts to report each one, except a
|
|
319
|
+
* background task started after the signal by a session whose turn was running when it came
|
|
320
|
+
* (`ThreadSession.restartReady`, issue #87). Queued turns end at once, asking to be sent
|
|
321
|
+
* again. An approval or a question stays open: the chat's connection lives until the drain
|
|
322
|
+
* ends, so the owner can still answer it. A hold does not: it asks about a session the
|
|
323
|
+
* restart is about to touch, so it is cancelled here exactly as `!stop` would (its own waiter
|
|
324
|
+
* tells the owner `Not sent.`).
|
|
325
|
+
*/
|
|
326
|
+
async drain(cutShort) {
|
|
327
|
+
this.draining = true;
|
|
328
|
+
const sessions = [...this.sessions.values()];
|
|
329
|
+
// (sync) Every flag before the first await: no worker sends a queued turn in between.
|
|
330
|
+
for (const session of sessions) {
|
|
331
|
+
session.draining = true;
|
|
332
|
+
session.mayHaveOrderedRestart = session.busy;
|
|
333
|
+
}
|
|
334
|
+
for (const session of sessions) {
|
|
335
|
+
await session.cancelHold();
|
|
336
|
+
// A queued turn genuinely dropped by the drain reacts ❌.
|
|
337
|
+
await session.dropQueued({ error: true });
|
|
338
|
+
}
|
|
339
|
+
while (!cutShort.aborted) {
|
|
340
|
+
const waiting = [...this.sessions.values()];
|
|
341
|
+
if (waiting.every((session) => session.restartReady && !session.reporting))
|
|
342
|
+
return;
|
|
343
|
+
for (const session of waiting) {
|
|
344
|
+
// a failed post must not end the wait
|
|
345
|
+
await session.showRestartWait().catch(() => { });
|
|
346
|
+
}
|
|
347
|
+
// Polled: a turn ends in several places, and a stop needs no finer timing.
|
|
348
|
+
await this.deps.clock.sleep(DRAIN_POLL_SECONDS, cutShort).catch(() => { });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Close every session and wait for each to be fully torn down, including one an idle close
|
|
353
|
+
* already had in flight: `close()` is safe to call again on a session already closing, but
|
|
354
|
+
* returning as soon as this call's own no-op finds the client already gone would not wait for
|
|
355
|
+
* whichever call is actually disconnecting it. From its first line the manager refuses to open,
|
|
356
|
+
* resume or bind, and hands out no session. `closed` is called after each session's close, so
|
|
357
|
+
* a caller can tell a long shutdown that goes on from one that is stuck.
|
|
358
|
+
*/
|
|
359
|
+
async closeAll(closed) {
|
|
360
|
+
this.closed = true;
|
|
361
|
+
const sessions = [...this.sessions.values()];
|
|
362
|
+
for (const session of sessions) {
|
|
363
|
+
if (session.closed)
|
|
364
|
+
continue;
|
|
365
|
+
try {
|
|
366
|
+
await session.close();
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
// one session's failure must not skip the rest
|
|
370
|
+
logger.error(`could not close ${session.channelId}/${session.threadId}: ${errorName(error)}`);
|
|
371
|
+
}
|
|
372
|
+
closed?.();
|
|
373
|
+
}
|
|
374
|
+
for (const session of sessions)
|
|
375
|
+
await session.doneClosing.wait();
|
|
376
|
+
this.sessions.clear();
|
|
377
|
+
}
|
|
378
|
+
}
|