dsh-bot-team 1.1.0 → 1.3.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/CHANGELOG.md +194 -0
- package/README.md +206 -156
- package/client.js +3706 -746
- package/cordis.patch.yml +2 -2
- package/host/agents.js +469 -289
- package/host/background-interactions.js +213 -0
- package/host/conversations.js +45 -0
- package/host/discussion-flow.js +179 -0
- package/host/discussion-runner.js +491 -0
- package/host/domain.js +551 -69
- package/host/engine.js +621 -166
- package/host/execution.js +192 -121
- package/host/native-activity.js +225 -0
- package/host/native-session.js +144 -0
- package/host/pdf.js +43 -6
- package/host/prompt.js +253 -9
- package/host/routes.js +31 -1
- package/host/service.js +751 -127
- package/host/session-export.js +22 -0
- package/host/session-visibility.js +147 -0
- package/host/storage-layout.js +109 -0
- package/host/tools.js +314 -85
- package/host/update-check.js +351 -0
- package/host/util.js +3 -2
- package/host/workspace.js +36 -2
- package/index.js +1 -1
- package/package.json +14 -6
package/host/agents.js
CHANGED
|
@@ -1,41 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Creation contract (M0-verified):
|
|
5
|
-
* agents.create({ sessionId, meta:{ cwd, origin:'subagent', agentPreset },
|
|
6
|
-
* agentOptions:{ provider, model, reasoningEffort?, maxTokens? },
|
|
7
|
-
* setup(agentCtx, agent) })
|
|
8
|
-
* Resume contract (M0 P2): `agents.resume` does NOT inherit the stored route, so
|
|
9
|
-
* every resume re-supplies agentOptions.
|
|
10
|
-
*
|
|
11
|
-
* setup() installs, for that one agent scope:
|
|
12
|
-
* - the full tool preset (files, search, web, bash, todo),
|
|
13
|
-
* - a `deployment:persona-prefix` section that replaces the deployment persona,
|
|
14
|
-
* - a `bot-team:group` runtime context with the live roster,
|
|
15
|
-
* - tool restrictions that block recursive Team tools,
|
|
16
|
-
* - approval policy `never` (background bots cannot answer human approvals).
|
|
2
|
+
* One hidden background DSH Session per Bot membership. The plugin owns
|
|
3
|
+
* factory-created/resumed handles and mounts the native default Agent preset.
|
|
17
4
|
*/
|
|
18
5
|
import { BOT_TEAM_TOOL_NAMES } from './tools.js';
|
|
19
6
|
import { newId } from './util.js';
|
|
20
|
-
import {
|
|
7
|
+
import { currentConversationId, conversationFloor, isCurrentMemberSession } from './conversations.js';
|
|
8
|
+
import { defaultWorkspaceDir, ensureWorkspaceDir, sessionCwdMatches, validateProjectWorkspace } from './workspace.js';
|
|
9
|
+
import { botModel, inspectSessionHeader, installBotModel, submitNativeInput } from './native-session.js';
|
|
21
10
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'subagent_fork',
|
|
36
|
-
'ask_user_question',
|
|
37
|
-
'workflow',
|
|
38
|
-
];
|
|
11
|
+
const RECURSIVE_TEAM_TOOLS = BOT_TEAM_TOOL_NAMES.filter(name => !['bot_team_list', 'bot_team_task_list'].includes(name));
|
|
12
|
+
// Native reminders deliver through SessionController, which rejects hidden
|
|
13
|
+
// origins. Leave list/delete available for old ordinary-session reminders.
|
|
14
|
+
const BACKGROUND_UNDELIVERABLE_TOOLS = ['schedule_create', 'schedule_update'];
|
|
15
|
+
const backgroundHeader = header => header?.origin === 'subagent'
|
|
16
|
+
&& header.parentSession === undefined && header.parentSessionId === undefined
|
|
17
|
+
&& header.parent === undefined && (header.delegationDepth === undefined || header.delegationDepth === 0);
|
|
18
|
+
const presetOf = (header, events) => {
|
|
19
|
+
const selected = events.findLast(event => event.type === 'agent-preset/selected');
|
|
20
|
+
if (!selected) return header.agentPreset;
|
|
21
|
+
if (typeof selected.data?.agentPreset !== 'string' || !selected.data.agentPreset) throw new Error('Invalid stored Bot preset selection');
|
|
22
|
+
return selected.data.agentPreset;
|
|
23
|
+
};
|
|
39
24
|
|
|
40
25
|
export class AgentPool {
|
|
41
26
|
constructor({ ctx, repo, personas, config, logger }) {
|
|
@@ -45,318 +30,513 @@ export class AgentPool {
|
|
|
45
30
|
this.config = config;
|
|
46
31
|
this.logger = logger;
|
|
47
32
|
this.entries = new Map();
|
|
33
|
+
this.owned = new Map();
|
|
34
|
+
this.preparing = new Map();
|
|
35
|
+
this.preparationAborts = new Map();
|
|
36
|
+
this.overlays = new Map();
|
|
37
|
+
this.overlayPreparing = new Map();
|
|
38
|
+
this.versions = new Map();
|
|
39
|
+
this.modelUpdates = new Map();
|
|
40
|
+
this.recordingModel = new WeakSet();
|
|
41
|
+
this.submitting = new Map();
|
|
42
|
+
this.modelWrites = new Map();
|
|
43
|
+
this.drainedSessions = new Set();
|
|
44
|
+
this.disposed = false;
|
|
45
|
+
// Observe factory publication and old ordinary-session activations. Owned
|
|
46
|
+
// setup already installs its overlay; repeated hooks remain idempotent.
|
|
47
|
+
this.offCreated = ctx.on('agent/created', async ({ agent }) => {
|
|
48
|
+
if (this.disposed) return;
|
|
49
|
+
const member = this.memberForSession(agent.id);
|
|
50
|
+
if (member && this.canOverlay(agent)) {
|
|
51
|
+
await this.makeSetup(member.membershipId)(agent.ctx, agent);
|
|
52
|
+
}
|
|
53
|
+
}, { global: true });
|
|
54
|
+
this.offDisposed = ctx.on('agent/disposed', ({ agent }) => {
|
|
55
|
+
this.detachOverlay(agent);
|
|
56
|
+
this.owned.delete(agent);
|
|
57
|
+
for (const [id, entry] of this.entries) if (entry.agent === agent) this.entries.delete(id);
|
|
58
|
+
}, { global: true });
|
|
59
|
+
this.offSelection = ctx.on('session/event', (session, event) => this.onModelSelection(session, event), { global: true });
|
|
60
|
+
this.ready = Promise.all([...(this.repo.members?.values() || [])].map(member => {
|
|
61
|
+
const agent = isCurrentMemberSession(this.repo, member) && this.ctx.agents.get(member.sessionId);
|
|
62
|
+
return agent && this.canOverlay(agent) ? this.makeSetup(member.membershipId)(agent.ctx, agent) : undefined;
|
|
63
|
+
})).then(() => undefined);
|
|
64
|
+
// Keep the rejection observable to service.ready / ensure without an
|
|
65
|
+
// unhandled rejection while the host is still constructing the service.
|
|
66
|
+
this.ready.catch(error => this.logger?.warn?.('bot-team: restore live overlays failed: ' + String(error)));
|
|
48
67
|
}
|
|
49
68
|
|
|
50
|
-
sessionIdOf(member) {
|
|
51
|
-
|
|
69
|
+
sessionIdOf(member) { return member.sessionId; }
|
|
70
|
+
|
|
71
|
+
memberForSession(sessionId) {
|
|
72
|
+
// The durable repository is the single mapping authority, including after
|
|
73
|
+
// restart and before create publishes agent/created. Never infer from IDs.
|
|
74
|
+
for (const member of this.repo.members?.values() || []) {
|
|
75
|
+
if (isCurrentMemberSession(this.repo, member, sessionId)) return member;
|
|
76
|
+
}
|
|
77
|
+
// Small embedders can expose only getMember; entries still stay verified.
|
|
78
|
+
for (const memberId of new Set([...this.preparing.keys(), ...this.entries.keys()])) {
|
|
79
|
+
const member = this.repo.getMember(memberId);
|
|
80
|
+
if (isCurrentMemberSession(this.repo, member, sessionId)) return member;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
requireMember(memberId) {
|
|
55
86
|
const member = this.repo.getMember(memberId);
|
|
56
|
-
if (!member) throw new Error(
|
|
87
|
+
if (!member) throw new Error('membership ' + memberId + ' does not exist');
|
|
57
88
|
const bot = this.repo.getBot(member.botId);
|
|
58
|
-
if (!bot) throw new Error(
|
|
89
|
+
if (!bot) throw new Error('bot ' + member.botId + ' of membership ' + memberId + ' does not exist');
|
|
59
90
|
const group = this.repo.getGroup(member.groupId);
|
|
60
|
-
if (!group) throw new Error(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const fingerprint = JSON.stringify({
|
|
64
|
-
provider: bot.provider,
|
|
65
|
-
model: bot.model,
|
|
66
|
-
reasoningEffort: bot.reasoningEffort ?? null,
|
|
67
|
-
maxTokens: bot.maxTokens ?? null,
|
|
68
|
-
cwd,
|
|
69
|
-
});
|
|
91
|
+
if (!group) throw new Error('group ' + member.groupId + ' does not exist');
|
|
92
|
+
return { member, bot, group };
|
|
93
|
+
}
|
|
70
94
|
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
95
|
+
async ensure(memberId) {
|
|
96
|
+
if (this.disposed) throw new Error('Bot AgentPool is disposed');
|
|
97
|
+
const { member } = this.requireMember(memberId);
|
|
98
|
+
this.assertAdmission?.(member.groupId);
|
|
99
|
+
const expected = this.generation(member);
|
|
100
|
+
if (this.preparing.has(memberId)) return this.preparing.get(memberId);
|
|
101
|
+
// Defer prepare until this map is populated: synchronous lifecycle hooks
|
|
102
|
+
// and callers joining in the same tick see the same in-flight operation.
|
|
103
|
+
const abort = new AbortController();
|
|
104
|
+
const version = this.versions.get(memberId);
|
|
105
|
+
this.preparationAborts.set(memberId, abort);
|
|
106
|
+
const pending = Promise.resolve().then(() => this.prepare(memberId, version, abort.signal, expected));
|
|
107
|
+
this.preparing.set(memberId, pending);
|
|
108
|
+
try { return await pending; }
|
|
109
|
+
finally {
|
|
110
|
+
if (this.preparing.get(memberId) === pending) this.preparing.delete(memberId);
|
|
111
|
+
if (this.preparationAborts.get(memberId) === abort) this.preparationAborts.delete(memberId);
|
|
74
112
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
isPreparingGroup(groupId) {
|
|
116
|
+
return [...this.preparing.keys(), ...this.submitting.keys(), ...[...this.overlayPreparing.values()].map(value => value.membershipId)]
|
|
117
|
+
.some(id => this.repo.getMember(id)?.groupId === groupId) || [...this.modelWrites.values()].includes(groupId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
generation(member) {
|
|
121
|
+
return { membershipId: member.membershipId, groupId: member.groupId, botId: member.botId,
|
|
122
|
+
conversationId: currentConversationId(this.repo, member.groupId), sessionId: member.sessionId };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
assertGeneration(expected, { current = false, admission = true } = {}) {
|
|
126
|
+
const member = this.repo.getMember(expected.membershipId);
|
|
127
|
+
if (this.disposed || !member || member.groupId !== expected.groupId || member.botId !== expected.botId ||
|
|
128
|
+
member.sessionId !== expected.sessionId || currentConversationId(this.repo, expected.groupId) !== expected.conversationId ||
|
|
129
|
+
(current && !isCurrentMemberSession(this.repo, member, expected.sessionId))) {
|
|
130
|
+
throw Object.assign(new Error('Bot session belongs to a stale conversation or was disposed'), { code: 'conversation-stale' });
|
|
82
131
|
}
|
|
132
|
+
if (admission) this.assertAdmission?.(expected.groupId);
|
|
133
|
+
return member;
|
|
134
|
+
}
|
|
83
135
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
...(typeof bot.reasoningEffort === 'string' && bot.reasoningEffort ? { reasoningEffort: bot.reasoningEffort } : {}),
|
|
89
|
-
...(Number.isInteger(bot.maxTokens) && bot.maxTokens > 0 ? { maxTokens: bot.maxTokens } : {}),
|
|
90
|
-
};
|
|
91
|
-
const setup = this.makeSetup(member.membershipId);
|
|
92
|
-
await ensureWorkspaceDir(cwd, this.logger);
|
|
93
|
-
const meta = {
|
|
94
|
-
cwd,
|
|
95
|
-
origin: 'subagent',
|
|
96
|
-
agentPreset: presetId,
|
|
97
|
-
};
|
|
136
|
+
assertPreparing(memberId, version, expected) {
|
|
137
|
+
if (this.disposed || this.versions.get(memberId) !== version) throw new Error('Bot session preparation was disposed');
|
|
138
|
+
if (expected) this.assertGeneration(expected);
|
|
139
|
+
}
|
|
98
140
|
|
|
99
|
-
|
|
141
|
+
owns(agent) { return this.owned.has(agent); }
|
|
142
|
+
|
|
143
|
+
canOverlay(agent) {
|
|
144
|
+
const header = agent.session?.header;
|
|
145
|
+
if (header?.origin !== undefined && header.origin !== 'subagent') return false;
|
|
146
|
+
if (!header || agent.parentAgent || header.parentSession !== undefined || header.parentSessionId !== undefined || header.parent !== undefined || (header.delegationDepth !== undefined && header.delegationDepth !== 0)) return false;
|
|
147
|
+
if (header.origin === 'subagent' && !backgroundHeader(header)) return false;
|
|
148
|
+
// Durable origin does not define runtime parentage. A malformed real child
|
|
149
|
+
// must not be borrowed just because it happens to match a membership id.
|
|
150
|
+
if (this.ctx.agents.get(agent.id) === agent && typeof this.ctx.agents.roots === 'function' && !this.ctx.agents.roots().includes(agent)) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async storedPreset(sessionId, header) {
|
|
155
|
+
const live = this.ctx.agents.get(sessionId);
|
|
156
|
+
if (live) return presetOf(header, live.session.snapshotEvents());
|
|
157
|
+
const observation = await this.ctx.get('sessionQuery').observeSession(sessionId, { projectionMode: 'none' });
|
|
158
|
+
try {
|
|
159
|
+
if (observation.header?.id !== sessionId || !Array.isArray(observation.events)) throw new Error('Invalid stored Bot preset history: ' + sessionId);
|
|
160
|
+
return presetOf(observation.header, observation.events);
|
|
161
|
+
} finally { observation[Symbol.dispose]?.(); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async releaseOwned(agent) {
|
|
165
|
+
const owned = this.owned.get(agent);
|
|
166
|
+
if (!owned) return;
|
|
167
|
+
owned.releasing ||= Promise.resolve().then(async () => {
|
|
168
|
+
// The native factory owns the root only; the subagent manager owns its
|
|
169
|
+
// descendants. Close that tree's admission and await its exact handles.
|
|
170
|
+
if (this.ctx.agents.get(agent.id) === agent) await this.ctx.get('subagents')?.drainDescendants?.([agent]);
|
|
171
|
+
await owned.handle.dispose();
|
|
172
|
+
this.drainedSessions.add(agent.id);
|
|
173
|
+
});
|
|
100
174
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.logger?.info?.(
|
|
108
|
-
`bot-team: resume ${member.sessionId} failed, creating a fresh bot session: ${String(
|
|
109
|
-
resumeError && resumeError.message ? resumeError.message : resumeError,
|
|
110
|
-
)}`,
|
|
111
|
-
);
|
|
112
|
-
handle = await this.ctx.agents.create({
|
|
113
|
-
sessionId: member.sessionId,
|
|
114
|
-
meta,
|
|
115
|
-
agentOptions,
|
|
116
|
-
setup,
|
|
117
|
-
});
|
|
175
|
+
await owned.releasing;
|
|
176
|
+
if (this.owned.get(agent) === owned) this.owned.delete(agent);
|
|
177
|
+
this.detachOverlay(agent);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
owned.releasing = undefined;
|
|
180
|
+
throw error;
|
|
118
181
|
}
|
|
182
|
+
}
|
|
119
183
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
async prepare(memberId, version, signal, expected) {
|
|
185
|
+
const check = () => { signal.throwIfAborted(); this.assertPreparing(memberId, version, expected); };
|
|
186
|
+
check();
|
|
187
|
+
await this.ready;
|
|
188
|
+
check();
|
|
189
|
+
const { member, group } = this.requireMember(memberId);
|
|
190
|
+
const cwd = group.workspaceCwd || defaultWorkspaceDir(this.config, group);
|
|
191
|
+
if (group.projectWorkspaceCwd) await validateProjectWorkspace(cwd);
|
|
192
|
+
else if (!(await ensureWorkspaceDir(cwd, this.logger))) throw new Error('无法准备默认工作目录');
|
|
193
|
+
check();
|
|
194
|
+
const presets = this.ctx.agentPresets || this.ctx.get('agentPresets');
|
|
195
|
+
if (typeof presets?.resolve !== 'function' || typeof presets?.mount !== 'function') throw new Error('Native default Agent preset registry is unavailable');
|
|
196
|
+
const staleConversation = expected.conversationId !== null && member.sessionConversationId !== expected.conversationId;
|
|
197
|
+
let sessionId = member.sessionId;
|
|
198
|
+
// A stale mapping never causes native observation/resume or id reuse, even
|
|
199
|
+
// when its old log was never written or several conversations were skipped.
|
|
200
|
+
let header = !staleConversation && sessionId ? await inspectSessionHeader(this.ctx, sessionId) : undefined;
|
|
201
|
+
check();
|
|
202
|
+
const live = !staleConversation && sessionId && this.ctx.agents.get(sessionId);
|
|
203
|
+
let compatible = false;
|
|
204
|
+
let preset;
|
|
205
|
+
let storedId;
|
|
206
|
+
if (header && backgroundHeader(header) && sessionCwdMatches(header.cwd, cwd) && (!live || this.canOverlay(live))) {
|
|
207
|
+
storedId = await this.storedPreset(sessionId, header);
|
|
208
|
+
check();
|
|
209
|
+
if (typeof storedId !== 'string' || !storedId) throw new Error('Invalid stored Bot preset identity');
|
|
210
|
+
if (storedId !== 'bot-team-tools') {
|
|
211
|
+
preset = await presets.resolve(storedId);
|
|
212
|
+
check();
|
|
213
|
+
if (preset?.id !== storedId || preset.broken) throw new Error('Invalid stored Bot preset: ' + storedId);
|
|
214
|
+
compatible = true;
|
|
134
215
|
}
|
|
135
|
-
const freshSessionId = newId('session-botteam');
|
|
136
|
-
this.logger?.info?.(
|
|
137
|
-
`bot-team: workspace ${cwd} does not match session ${previousSessionId} cwd ${String(
|
|
138
|
-
sessionCwd,
|
|
139
|
-
)}; starting fresh session ${freshSessionId}`,
|
|
140
|
-
);
|
|
141
|
-
handle = await this.ctx.agents.create({ sessionId: freshSessionId, meta, agentOptions, setup });
|
|
142
|
-
member.sessionId = freshSessionId;
|
|
143
|
-
await this.repo.putMember(member);
|
|
144
216
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
217
|
+
if (!compatible) {
|
|
218
|
+
preset = await presets.resolve(undefined);
|
|
219
|
+
check();
|
|
220
|
+
if (typeof preset?.id !== 'string' || !preset.id || preset.broken) throw new Error('Native default Agent preset is invalid');
|
|
221
|
+
compatible = !staleConversation && storedId === preset.id;
|
|
222
|
+
}
|
|
223
|
+
const rotated = staleConversation || Boolean(header && !compatible);
|
|
224
|
+
const next = { ...member };
|
|
225
|
+
if (rotated) {
|
|
226
|
+
if (sessionId) {
|
|
227
|
+
if (typeof this.repo.bindSession === 'function') {
|
|
228
|
+
await this.repo.bindSession(member, { sessionId, conversationId: member.sessionConversationId, reason: 'retained-history' });
|
|
229
|
+
check();
|
|
230
|
+
}
|
|
231
|
+
next.previousSessionIds = [...new Set([...(member.previousSessionIds || []), sessionId])];
|
|
156
232
|
}
|
|
233
|
+
sessionId = newId('session-botteam');
|
|
234
|
+
header = undefined;
|
|
235
|
+
next.lastSeenSeq = conversationFloor(this.repo, group.groupId);
|
|
236
|
+
}
|
|
237
|
+
if (!sessionId) sessionId = newId('session-botteam');
|
|
238
|
+
next.sessionId = sessionId;
|
|
239
|
+
if (expected.conversationId !== null) next.sessionConversationId = expected.conversationId;
|
|
240
|
+
if (rotated) {
|
|
241
|
+
// Release all old handles owned by this membership, including detached
|
|
242
|
+
// cache entries. Historical borrowed agents retain their original owner.
|
|
243
|
+
this.entries.delete(memberId);
|
|
244
|
+
for (const [agent, owned] of [...this.owned]) if (owned.membershipId === memberId) {
|
|
245
|
+
await this.releaseOwned(agent);
|
|
246
|
+
check();
|
|
247
|
+
}
|
|
248
|
+
for (const [agent, overlay] of [...this.overlays]) if (overlay.membershipId === memberId) this.detachOverlay(agent);
|
|
249
|
+
}
|
|
250
|
+
if (typeof this.repo.bindSession === 'function') {
|
|
251
|
+
await this.repo.bindSession(next, { sessionId, conversationId: expected.conversationId, reason: rotated ? 'session-rotation' : 'session-activation' });
|
|
252
|
+
check();
|
|
253
|
+
}
|
|
254
|
+
if (!header || next.sessionConversationId !== member.sessionConversationId) {
|
|
255
|
+
// The new mapping is published only after strict persistence succeeds.
|
|
256
|
+
if (typeof this.repo.putMemberStrict === 'function') {
|
|
257
|
+
await this.repo.putMemberStrict(next, { expectedConversationId: expected.conversationId, expectedSessionId: expected.sessionId });
|
|
258
|
+
} else {
|
|
259
|
+
// Legacy repository doubles preserve object identity. Real repositories
|
|
260
|
+
// use the strict copy/write/publish path above.
|
|
261
|
+
const before = { ...member };
|
|
262
|
+
Object.assign(member, next);
|
|
263
|
+
try { await this.repo.putMember(member); }
|
|
264
|
+
catch (error) { Object.assign(member, before); throw error; }
|
|
265
|
+
}
|
|
266
|
+
expected = { ...expected, sessionId };
|
|
267
|
+
check();
|
|
157
268
|
}
|
|
158
269
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
sessionId: member.sessionId,
|
|
162
|
-
handle,
|
|
163
|
-
fingerprint,
|
|
164
|
-
agent: handle.agent,
|
|
165
|
-
});
|
|
166
|
-
await this.applySandboxMode(handle.agent);
|
|
167
|
-
return handle.agent;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* All bots are full-capability, so the session sandbox is workspace-write.
|
|
172
|
-
* The mode is a normal session event (the canonical write path), appended
|
|
173
|
-
* only when it differs. Group cwd still bounds the filesystem.
|
|
174
|
-
*/
|
|
175
|
-
async applySandboxMode(agent) {
|
|
176
|
-
if (!this.config.botSandbox) return;
|
|
177
|
-
const desired = 'workspace-write';
|
|
270
|
+
let handle;
|
|
271
|
+
let agent = compatible ? live : undefined;
|
|
178
272
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
273
|
+
if (!agent) {
|
|
274
|
+
const setup = async (scope, prepared) => {
|
|
275
|
+
check();
|
|
276
|
+
if (!backgroundHeader(prepared.session.header) || !sessionCwdMatches(prepared.session.header.cwd, cwd)) throw new Error('Background Bot session metadata changed during activation: ' + sessionId);
|
|
277
|
+
await presets.mount(scope, preset.id);
|
|
278
|
+
check();
|
|
279
|
+
await this.makeSetup(memberId)(scope, prepared);
|
|
280
|
+
check();
|
|
281
|
+
};
|
|
282
|
+
const options = { agentOptions: botModel(this.requireMember(memberId).bot), setup, signal };
|
|
283
|
+
handle = header
|
|
284
|
+
? await this.ctx.agents.resume({ ...options, resumeSessionId: sessionId })
|
|
285
|
+
: await this.ctx.agents.create({ ...options, sessionId, meta: { cwd, origin: 'subagent', agentPreset: preset.id } });
|
|
286
|
+
agent = handle?.agent;
|
|
287
|
+
if (agent) this.owned.set(agent, { membershipId: memberId, conversationId: expected.conversationId, handle });
|
|
288
|
+
if (!agent || agent.id !== sessionId || typeof handle.dispose !== 'function') throw new Error('Background Agent factory returned an unexpected Bot session: ' + sessionId);
|
|
289
|
+
}
|
|
290
|
+
check();
|
|
291
|
+
if (!backgroundHeader(agent.session.header) || !this.canOverlay(agent) || !sessionCwdMatches(agent.session.header.cwd, cwd)) throw new Error('Background Bot session metadata changed during activation: ' + sessionId);
|
|
292
|
+
await this.makeSetup(memberId)(agent.ctx, agent);
|
|
293
|
+
check();
|
|
294
|
+
const workingDirectory = this.ctx.get('workingDirectory');
|
|
295
|
+
if (typeof workingDirectory?.set === 'function') {
|
|
296
|
+
try { await workingDirectory.set(agent, cwd); }
|
|
297
|
+
catch (error) { throw new Error('无法设置 Bot 工作目录 ' + cwd + ': ' + String(error?.message || error), { cause: error }); }
|
|
184
298
|
}
|
|
299
|
+
check();
|
|
300
|
+
if (this.ctx.agents.get(sessionId) !== agent) throw new Error('Bot session was disposed during activation');
|
|
301
|
+
this.assertGeneration(expected, { current: true });
|
|
302
|
+
this.entries.set(memberId, { membershipId: memberId, sessionId, conversationId: expected.conversationId, agent });
|
|
303
|
+
return agent;
|
|
185
304
|
} catch (error) {
|
|
186
|
-
|
|
305
|
+
if (handle && agent) await this.releaseOwned(agent);
|
|
306
|
+
throw error;
|
|
187
307
|
}
|
|
188
308
|
}
|
|
189
309
|
|
|
310
|
+
async applySandboxMode(agent) {
|
|
311
|
+
if (!this.config.botSandbox) return;
|
|
312
|
+
const policy = this.ctx.get('sandboxPolicy');
|
|
313
|
+
const current = policy?.overrideOf?.(agent.session)
|
|
314
|
+
?? agent.session.snapshotEvents?.().findLast(event => event.type === 'sandbox/mode')?.data.mode;
|
|
315
|
+
if (current !== 'workspace-write') agent.session.append('sandbox/mode', { mode: 'workspace-write' });
|
|
316
|
+
}
|
|
317
|
+
|
|
190
318
|
current(memberId) {
|
|
319
|
+
if (this.disposed) return undefined;
|
|
320
|
+
const member = this.repo.getMember(memberId);
|
|
321
|
+
const agent = isCurrentMemberSession(this.repo, member) && this.ctx.agents.get(member.sessionId);
|
|
191
322
|
const entry = this.entries.get(memberId);
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
return entry.agent;
|
|
323
|
+
if (entry && (entry.sessionId !== member?.sessionId || entry.agent !== agent)) this.entries.delete(memberId);
|
|
324
|
+
// Borrowed background roots and legacy native activations may bypass ensure().
|
|
325
|
+
const overlay = agent && this.overlays.get(agent);
|
|
326
|
+
return overlay?.membershipId === memberId && overlay.conversationId === currentConversationId(this.repo, member.groupId) ? agent : undefined;
|
|
198
327
|
}
|
|
199
328
|
|
|
200
329
|
makeSetup(membershipId) {
|
|
201
330
|
return async (agentCtx, agent) => {
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} catch (error) {
|
|
215
|
-
this.logger?.warn?.(`bot-team: mount preset ${presetId} failed: ${String(error)}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 2) identity: shadow the deployment persona for this agent only
|
|
220
|
-
try {
|
|
221
|
-
agentCtx.systemPrompt.section({
|
|
222
|
-
name: 'deployment:persona-prefix',
|
|
223
|
-
order: 0,
|
|
224
|
-
text: () => {
|
|
225
|
-
// Lazy import-free rebuild each assembly from current repo state.
|
|
226
|
-
const currentMember = this.repo.getMember(membershipId);
|
|
227
|
-
if (!currentMember) return '';
|
|
228
|
-
return this.personaText(currentMember.botId, currentMember.groupId);
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
} catch (error) {
|
|
232
|
-
this.logger?.warn?.(`bot-team: register persona section failed: ${String(error)}`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 3) live roster runtime context
|
|
236
|
-
try {
|
|
237
|
-
agentCtx.systemPrompt.context({
|
|
238
|
-
name: 'bot-team:group',
|
|
239
|
-
order: 600,
|
|
240
|
-
text: () => {
|
|
241
|
-
const currentMember = this.repo.getMember(membershipId);
|
|
242
|
-
if (!currentMember) return '';
|
|
243
|
-
return this.groupContextText(currentMember.botId, currentMember.groupId);
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
} catch (error) {
|
|
247
|
-
this.logger?.warn?.(`bot-team: register group context failed: ${String(error)}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// 4) tool restrictions: Team tools only (no recursive group driving)
|
|
251
|
-
try {
|
|
252
|
-
const tools = this.ctx.get('tools');
|
|
253
|
-
const deny = [];
|
|
254
|
-
for (const name of [...TEAM_TOOL_CANDIDATES, ...BOT_TEAM_TOOL_NAMES]) {
|
|
255
|
-
if (tools && typeof tools.get === 'function' && tools.get(name) !== undefined && !deny.includes(name)) {
|
|
256
|
-
deny.push(name);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (deny.length > 0 && agentCtx.tools && typeof agentCtx.tools.restrict === 'function') {
|
|
260
|
-
agentCtx.tools.restrict({ deny });
|
|
261
|
-
}
|
|
262
|
-
} catch (error) {
|
|
263
|
-
this.logger?.warn?.(`bot-team: restrict bot tools failed: ${String(error)}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 5) forward live assistant stream frames (agent-scoped event) to the engine
|
|
267
|
-
try {
|
|
268
|
-
if (typeof agentCtx.on === 'function') {
|
|
269
|
-
agentCtx.on('agent/assistant-stream', (payload) => {
|
|
270
|
-
try {
|
|
271
|
-
this.streamHandler?.({ ...payload, agent });
|
|
272
|
-
} catch {
|
|
273
|
-
/* noop */
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
} catch (error) {
|
|
278
|
-
this.logger?.warn?.(`bot-team: subscribe assistant stream failed: ${String(error)}`);
|
|
279
|
-
}
|
|
331
|
+
if (this.disposed || !this.canOverlay(agent) || !isCurrentMemberSession(this.repo, this.repo.getMember(membershipId), agent.id)) return;
|
|
332
|
+
const pending = this.overlayPreparing.get(agent);
|
|
333
|
+
if (pending) return pending.promise;
|
|
334
|
+
const operation = {
|
|
335
|
+
membershipId,
|
|
336
|
+
promise: Promise.resolve().then(() => this.installOverlay(membershipId, agentCtx, agent)),
|
|
337
|
+
};
|
|
338
|
+
this.overlayPreparing.set(agent, operation);
|
|
339
|
+
try { return await operation.promise; }
|
|
340
|
+
finally { if (this.overlayPreparing.get(agent) === operation) this.overlayPreparing.delete(agent); }
|
|
341
|
+
};
|
|
342
|
+
}
|
|
280
343
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
344
|
+
async installOverlay(membershipId, agentCtx, agent) {
|
|
345
|
+
if (this.disposed) return;
|
|
346
|
+
const { member } = this.requireMember(membershipId);
|
|
347
|
+
if (!isCurrentMemberSession(this.repo, member, agent.id)) return;
|
|
348
|
+
const expected = this.generation(member);
|
|
349
|
+
this.assertGeneration(expected, { current: true });
|
|
350
|
+
if (this.overlays.has(agent)) return;
|
|
351
|
+
const overlay = { membershipId, conversationId: expected.conversationId, disposers: [] };
|
|
352
|
+
const check = () => {
|
|
353
|
+
this.assertGeneration(expected, { current: true });
|
|
354
|
+
if (this.overlays.get(agent) !== overlay) throw new Error('Bot overlay was disposed');
|
|
355
|
+
};
|
|
356
|
+
const retain = disposer => { if (typeof disposer === 'function') overlay.disposers.push(disposer); };
|
|
357
|
+
this.overlays.set(agent, overlay);
|
|
358
|
+
try {
|
|
359
|
+
// Independent section: keep the harness identity, deployment persona,
|
|
360
|
+
// default preset, and the preset's entire native capability composition.
|
|
361
|
+
retain(agentCtx.systemPrompt.section({
|
|
362
|
+
name: 'bot-team:persona', order: 100,
|
|
363
|
+
text: context => {
|
|
364
|
+
if (context?.agent && context.agent !== agent) return '';
|
|
365
|
+
const member = this.repo.getMember(membershipId);
|
|
366
|
+
return isCurrentMemberSession(this.repo, member, agent.id) && expected.conversationId === currentConversationId(this.repo, member.groupId) ? this.personaText(member.botId, member.groupId) : '';
|
|
367
|
+
},
|
|
368
|
+
}));
|
|
369
|
+
retain(agentCtx.systemPrompt.context({
|
|
370
|
+
name: 'bot-team:group', order: 600,
|
|
371
|
+
text: context => {
|
|
372
|
+
if (context?.agent && context.agent !== agent) return '';
|
|
373
|
+
const member = this.repo.getMember(membershipId);
|
|
374
|
+
return isCurrentMemberSession(this.repo, member, agent.id) && expected.conversationId === currentConversationId(this.repo, member.groupId) ? this.groupContextText(member.botId, member.groupId) : '';
|
|
375
|
+
},
|
|
376
|
+
}));
|
|
377
|
+
const tools = this.ctx.get('tools');
|
|
378
|
+
const forbidden = [...RECURSIVE_TEAM_TOOLS, ...(backgroundHeader(agent.session.header) ? BACKGROUND_UNDELIVERABLE_TOOLS : [])];
|
|
379
|
+
const deny = forbidden.filter(name => tools?.get?.(name, agent) !== undefined);
|
|
380
|
+
if (deny.length) {
|
|
381
|
+
if (typeof agentCtx.tools?.restrict !== 'function') throw new Error('Native scoped tool restrictions are unavailable');
|
|
382
|
+
retain(agentCtx.tools.restrict({ deny }));
|
|
293
383
|
}
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (approval && typeof approval.setPolicy === 'function') {
|
|
302
|
-
approval.setPolicy(agent, this.config.botApprovalPolicy === 'never' ? 'never' : 'ask');
|
|
303
|
-
}
|
|
304
|
-
} catch (error) {
|
|
305
|
-
this.logger?.warn?.(`bot-team: set approval policy failed: ${String(error)}`);
|
|
384
|
+
// A monotonic native guard also covers tools registered after this
|
|
385
|
+
// overlay (for example while the plugin is hot-reloading).
|
|
386
|
+
if (typeof agentCtx.tools?.guard === 'function') {
|
|
387
|
+
retain(agentCtx.tools.guard(exec => RECURSIVE_TEAM_TOOLS.includes(exec.name)
|
|
388
|
+
? 'Bot Team members cannot recursively drive Bot Team write tools'
|
|
389
|
+
: backgroundHeader(agent.session.header) && BACKGROUND_UNDELIVERABLE_TOOLS.includes(exec.name)
|
|
390
|
+
? 'Native reminders cannot deliver to hidden Bot Team sessions' : undefined));
|
|
306
391
|
}
|
|
307
|
-
|
|
392
|
+
await installBotModel(this.ctx, agentCtx, agent, () => this.modelFor(membershipId, expected), selected => this.publishModelSelection(agent, selected, expected), retain, check);
|
|
393
|
+
check();
|
|
394
|
+
retain(agentCtx.on('agent/assistant-stream', payload => {
|
|
395
|
+
if (payload.agent && payload.agent !== agent) return;
|
|
396
|
+
if (this.current(membershipId) !== agent || expected.conversationId !== currentConversationId(this.repo, expected.groupId)) return;
|
|
397
|
+
this.streamHandler?.({ ...payload, agent });
|
|
398
|
+
}));
|
|
399
|
+
retain(agentCtx.on('agent/inbox/claimed', payload => {
|
|
400
|
+
if (payload.agent && payload.agent !== agent) return;
|
|
401
|
+
if (this.current(membershipId) !== agent || expected.conversationId !== currentConversationId(this.repo, expected.groupId)) return;
|
|
402
|
+
this.claimHandler?.({ ...payload, agent });
|
|
403
|
+
}));
|
|
404
|
+
const approval = this.ctx.get('approval');
|
|
405
|
+
approval?.setPolicy?.(agent, this.config.botApprovalPolicy === 'never' ? 'never' : 'ask');
|
|
406
|
+
await this.applySandboxMode(agent);
|
|
407
|
+
check();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.detachOverlay(agent);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
308
412
|
}
|
|
309
413
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
414
|
+
detachOverlay(agent) {
|
|
415
|
+
const overlay = this.overlays.get(agent);
|
|
416
|
+
if (!overlay) return;
|
|
417
|
+
this.overlays.delete(agent);
|
|
418
|
+
for (const dispose of overlay.disposers.reverse()) {
|
|
419
|
+
try { dispose(); }
|
|
420
|
+
catch (error) { this.logger?.warn?.('bot-team: detach overlay failed: ' + String(error)); }
|
|
421
|
+
}
|
|
314
422
|
}
|
|
315
423
|
|
|
316
|
-
|
|
317
|
-
|
|
424
|
+
onModelSelection(session, event) {
|
|
425
|
+
if (this.disposed || event.type !== 'model/selection' || this.recordingModel.has(session)) return;
|
|
426
|
+
const member = this.memberForSession(session.id);
|
|
427
|
+
if (!member) return;
|
|
428
|
+
const expected = this.generation(member);
|
|
429
|
+
const selected = { ...event.data };
|
|
430
|
+
const previous = this.modelUpdates.get(member.botId);
|
|
431
|
+
// A new native selection can retry a failed profile write. Within one Bot
|
|
432
|
+
// these writes stay ordered, including selections from different Sessions.
|
|
433
|
+
const write = (previous ? previous.catch(() => undefined) : Promise.resolve()).then(async () => {
|
|
434
|
+
this.assertGeneration(expected, { current: true });
|
|
435
|
+
const current = this.repo.getBot(member.botId);
|
|
436
|
+
if (!current) throw new Error('Bot no longer exists for native model selection');
|
|
437
|
+
const next = { ...current, provider: selected.provider, model: selected.model };
|
|
438
|
+
if (selected.reasoningEffort === undefined) delete next.reasoningEffort;
|
|
439
|
+
else next.reasoningEffort = selected.reasoningEffort;
|
|
440
|
+
await this.repo.putBot(next);
|
|
441
|
+
this.assertGeneration(expected, { current: true });
|
|
442
|
+
});
|
|
443
|
+
this.modelUpdates.set(member.botId, write);
|
|
444
|
+
this.modelWrites.set(write, member.groupId);
|
|
445
|
+
write.then(() => this.modelWrites.delete(write), () => this.modelWrites.delete(write));
|
|
446
|
+
write.catch(error => this.logger?.warn?.('bot-team: persist native model selection failed: ' + String(error)));
|
|
447
|
+
return write;
|
|
318
448
|
}
|
|
319
449
|
|
|
320
|
-
|
|
321
|
-
|
|
450
|
+
async modelFor(membershipId, expected) {
|
|
451
|
+
const { member } = this.requireMember(membershipId);
|
|
452
|
+
// New UI selections can arrive while a previous write is settling.
|
|
453
|
+
for (;;) {
|
|
454
|
+
const pending = this.modelUpdates.get(member.botId);
|
|
455
|
+
if (pending) await pending;
|
|
456
|
+
if (expected) this.assertGeneration(expected, { current: true });
|
|
457
|
+
if (pending === this.modelUpdates.get(member.botId)) return this.requireMember(membershipId).bot;
|
|
458
|
+
}
|
|
322
459
|
}
|
|
323
460
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
461
|
+
publishModelSelection(agent, selected, expected) {
|
|
462
|
+
if (expected) this.assertGeneration(expected, { current: true });
|
|
463
|
+
const projections = this.ctx.get('sessionProjections');
|
|
464
|
+
if (typeof projections?.stateOf !== 'function') throw new Error('Native modelSelection projection is unavailable');
|
|
465
|
+
const state = projections.stateOf(agent.session, 'modelSelection');
|
|
466
|
+
if (!state) throw new Error('Native modelSelection projection is not registered');
|
|
467
|
+
const next = state.pending ?? state.lastUsed;
|
|
468
|
+
const { maxTokens, ...intent } = selected;
|
|
469
|
+
if (next?.provider === intent.provider && next.model === intent.model && next.reasoningEffort === intent.reasoningEffort) return;
|
|
470
|
+
// Session.append dispatches synchronously. Ignore only this plugin's own
|
|
471
|
+
// publication; native UI selectModel events update the durable Bot profile.
|
|
472
|
+
this.recordingModel.add(agent.session);
|
|
473
|
+
try { agent.session.append('model/selection', intent); }
|
|
474
|
+
finally { this.recordingModel.delete(agent.session); }
|
|
327
475
|
}
|
|
328
476
|
|
|
477
|
+
personaText(botId, groupId) { return this.prompt?.renderPersona(botId, groupId) || ''; }
|
|
478
|
+
groupContextText(botId, groupId) { return this.prompt?.renderGroupContext(botId, groupId) || ''; }
|
|
479
|
+
attachPrompt(promptApi) { this.prompt = promptApi; }
|
|
480
|
+
attachStreamHandler(handler) { this.streamHandler = handler; }
|
|
329
481
|
attachClaimHandler(handler) {
|
|
330
482
|
this.claimHandler = handler;
|
|
331
|
-
return () => {
|
|
332
|
-
|
|
483
|
+
return () => { if (this.claimHandler === handler) this.claimHandler = undefined; };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async submit(agent, input, assertActive = () => {}) {
|
|
487
|
+
if (this.disposed) throw new Error('Bot AgentPool is disposed');
|
|
488
|
+
const member = this.memberForSession(agent.id);
|
|
489
|
+
if (!member) throw new Error('Bot input targets an unmapped or stale conversation native session');
|
|
490
|
+
const expected = this.generation(member);
|
|
491
|
+
const version = this.versions.get(member.membershipId);
|
|
492
|
+
const check = () => {
|
|
493
|
+
this.assertPreparing(member.membershipId, version, expected);
|
|
494
|
+
this.assertGeneration(expected, { current: true });
|
|
495
|
+
if (this.ctx.agents.get(agent.id) !== agent) throw new Error('Bot session was disposed during input admission');
|
|
496
|
+
assertActive();
|
|
333
497
|
};
|
|
498
|
+
check();
|
|
499
|
+
const pending = Promise.resolve().then(async () => {
|
|
500
|
+
const model = botModel(await this.modelFor(member.membershipId, expected));
|
|
501
|
+
check();
|
|
502
|
+
await submitNativeInput(this.ctx, agent, input, model, check);
|
|
503
|
+
check();
|
|
504
|
+
});
|
|
505
|
+
const operations = this.submitting.get(member.membershipId) || new Set();
|
|
506
|
+
operations.add(pending);
|
|
507
|
+
this.submitting.set(member.membershipId, operations);
|
|
508
|
+
try { return await pending; }
|
|
509
|
+
finally {
|
|
510
|
+
operations.delete(pending);
|
|
511
|
+
if (operations.size === 0) this.submitting.delete(member.membershipId);
|
|
512
|
+
}
|
|
334
513
|
}
|
|
335
514
|
|
|
336
515
|
async disposeMember(membershipId) {
|
|
337
|
-
|
|
338
|
-
|
|
516
|
+
this.versions.set(membershipId, (this.versions.get(membershipId) || 0) + 1);
|
|
517
|
+
this.preparationAborts.get(membershipId)?.abort(new Error('Bot session preparation was disposed'));
|
|
339
518
|
this.entries.delete(membershipId);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
519
|
+
const pending = this.preparing.get(membershipId);
|
|
520
|
+
const overlays = [...this.overlayPreparing.values()].filter(operation => operation.membershipId === membershipId);
|
|
521
|
+
await Promise.allSettled([pending, ...overlays.map(operation => operation.promise), ...(this.submitting.get(membershipId) || [])]);
|
|
522
|
+
for (const [agent, owned] of [...this.owned]) if (owned.membershipId === membershipId) await this.releaseOwned(agent);
|
|
523
|
+
for (const [agent, overlay] of this.overlays) if (overlay.membershipId === membershipId) this.detachOverlay(agent);
|
|
345
524
|
}
|
|
346
525
|
|
|
347
526
|
async disposeAll() {
|
|
348
|
-
|
|
527
|
+
this.disposed = true;
|
|
528
|
+
for (const abort of this.preparationAborts.values()) abort.abort(new Error('Bot AgentPool is disposed'));
|
|
529
|
+
this.offCreated?.();
|
|
530
|
+
this.offDisposed?.();
|
|
531
|
+
this.offSelection?.();
|
|
532
|
+
await Promise.allSettled([this.ready, ...this.modelUpdates.values(), ...this.preparing.values(), ...[...this.overlayPreparing.values()].map(operation => operation.promise), ...[...this.submitting.values()].flatMap(operations => [...operations])]);
|
|
533
|
+
const releases = await Promise.allSettled([...this.owned.keys()].map(agent => this.releaseOwned(agent)));
|
|
534
|
+
for (const result of releases) if (result.status === 'rejected') this.logger?.warn?.('bot-team: release background Agent failed: ' + String(result.reason));
|
|
349
535
|
this.entries.clear();
|
|
350
|
-
for (const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
} catch (error) {
|
|
354
|
-
this.logger?.warn?.(`bot-team: dispose agent ${entry.membershipId} failed: ${String(error)}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
536
|
+
for (const agent of this.overlays.keys()) this.detachOverlay(agent);
|
|
537
|
+
this.streamHandler = undefined;
|
|
538
|
+
this.claimHandler = undefined;
|
|
357
539
|
}
|
|
358
540
|
|
|
359
|
-
list() {
|
|
360
|
-
return [...this.entries.keys()];
|
|
361
|
-
}
|
|
541
|
+
list() { return [...new Set([...this.entries.keys(), ...[...this.overlays.values()].map(overlay => overlay.membershipId)])].filter(id => this.current(id)); }
|
|
362
542
|
}
|