@borgee/agents-host 0.2.2 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -21
- package/dist/agents-host-supervisor.d.ts +7 -5
- package/dist/agents-host-supervisor.js +24 -4
- package/dist/agents-host.d.ts +114 -15
- package/dist/agents-host.js +2712 -141
- package/dist/chat/chat-control-plane.d.ts +13 -2
- package/dist/chat/sdk-chat-control-plane.d.ts +14 -3
- package/dist/chat/sdk-chat-control-plane.js +54 -2
- package/dist/cli-args.d.ts +46 -5
- package/dist/cli-args.js +313 -32
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +112 -5
- package/dist/compatibility-gates.d.ts +39 -0
- package/dist/compatibility-gates.js +131 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +23 -5
- package/dist/connections-state-store.d.ts +81 -0
- package/dist/connections-state-store.js +228 -0
- package/dist/context/attention.d.ts +12 -0
- package/dist/context/attention.js +137 -0
- package/dist/context/collaboration-capabilities-diagnostics.d.ts +3 -0
- package/dist/context/collaboration-capabilities-diagnostics.js +18 -0
- package/dist/context/collaboration-outcome.d.ts +2 -0
- package/dist/context/collaboration-outcome.js +26 -0
- package/dist/context/injection.d.ts +134 -0
- package/dist/context/injection.js +355 -0
- package/dist/context/prompt.d.ts +4 -1
- package/dist/context/prompt.js +231 -1
- package/dist/context/task-thread-collaboration.d.ts +6 -0
- package/dist/context/task-thread-collaboration.js +31 -0
- package/dist/context/turn-preparation.d.ts +9 -0
- package/dist/context/turn-preparation.js +135 -0
- package/dist/debug.d.ts +44 -0
- package/dist/debug.js +135 -0
- package/dist/gateway/localhost-gateway.d.ts +52 -0
- package/dist/gateway/localhost-gateway.js +857 -0
- package/dist/index.js +7 -5
- package/dist/local-config.d.ts +4 -1
- package/dist/local-config.js +24 -7
- package/dist/managed-daemon-log.d.ts +34 -0
- package/dist/managed-daemon-log.js +261 -0
- package/dist/managed-daemon.d.ts +220 -0
- package/dist/managed-daemon.js +1601 -0
- package/dist/policy/authorization-audit.d.ts +63 -0
- package/dist/policy/authorization-audit.js +94 -0
- package/dist/policy/copilot-permission.d.ts +15 -0
- package/dist/policy/copilot-permission.js +193 -0
- package/dist/policy/gateway-authorization.d.ts +42 -0
- package/dist/policy/gateway-authorization.js +162 -0
- package/dist/providers/awaiting-user.d.ts +12 -0
- package/dist/providers/awaiting-user.js +192 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +8 -12
- package/dist/providers/claude/cli-client.d.ts +12 -5
- package/dist/providers/claude/cli-client.js +184 -37
- package/dist/providers/claude/session-store.d.ts +1 -0
- package/dist/providers/codex/adapter.d.ts +11 -0
- package/dist/providers/codex/adapter.js +19 -0
- package/dist/providers/codex/cli-client.d.ts +103 -0
- package/dist/providers/codex/cli-client.js +1133 -0
- package/dist/providers/codex/project-doc.d.ts +3 -0
- package/dist/providers/codex/project-doc.js +90 -0
- package/dist/providers/codex/session-store.d.ts +38 -0
- package/dist/providers/codex/session-store.js +150 -0
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +8 -12
- package/dist/providers/copilot/cli-client.d.ts +20 -2
- package/dist/providers/copilot/cli-client.js +251 -71
- package/dist/providers/copilot/session-store.d.ts +1 -0
- package/dist/providers/create-provider.d.ts +11 -2
- package/dist/providers/create-provider.js +131 -12
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -2
- package/dist/state-paths.d.ts +14 -1
- package/dist/state-paths.js +87 -3
- package/dist/task-thread-resolution.d.ts +10 -0
- package/dist/task-thread-resolution.js +48 -0
- package/dist/types.d.ts +267 -1
- package/dist/visible-mentions.d.ts +3 -0
- package/dist/visible-mentions.js +15 -0
- package/package.json +19 -17
- package/skills/borgee-agent/SKILL.md +33 -0
- package/skills/borgee-agent/borgee-agent.mjs +473 -0
- package/skills/borgee-agent/borgee-agent.py +409 -0
package/dist/agents-host.js
CHANGED
|
@@ -1,33 +1,280 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, link, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
4
|
import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
|
|
5
|
+
import { HostLogger, summarizeError } from './debug.js';
|
|
3
6
|
import { createDurableCursorStore } from './durable-cursor-store.js';
|
|
7
|
+
import { ATTENTION_FOLLOW_SEMANTICS_COMPATIBILITY_GATE, COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE, COLLABORATION_OUTCOME_MODEL_COMPATIBILITY_GATE, COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE, TOKEN_BINDING_COMPATIBILITY_GATE, } from './compatibility-gates.js';
|
|
8
|
+
import { buildNormalizedAttentionSnapshot, parsePersistedAttentionSnapshot, } from './context/attention.js';
|
|
9
|
+
import { projectTaskThreadCollaborationContract } from './context/task-thread-collaboration.js';
|
|
10
|
+
import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS, } from './gateway/localhost-gateway.js';
|
|
11
|
+
import { extractTaskIdFromTaskAssignmentContent, resolveChannelContextPayloadPath, } from './context/injection.js';
|
|
12
|
+
import { AuthorizationAuditSink, } from './policy/authorization-audit.js';
|
|
4
13
|
import { createProvider } from './providers/create-provider.js';
|
|
14
|
+
import { resolveAttentionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
|
|
15
|
+
import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
|
|
16
|
+
import { waitForTaskForThread } from './task-thread-resolution.js';
|
|
5
17
|
const PRIVATE_STATE_ROOT_MODE = 0o700;
|
|
6
18
|
const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
|
|
19
|
+
const PROTOCOL_STATE_DIRNAME = 'protocol-state';
|
|
20
|
+
const PROTOCOL_PROVIDER_TURN_TIMEOUT_MS = 120_000;
|
|
21
|
+
const PROTOCOL_KICKOFF_DECISION_TIMEOUT_MS = PROTOCOL_PROVIDER_TURN_TIMEOUT_MS;
|
|
22
|
+
const PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS = 50;
|
|
23
|
+
const PROTOCOL_KICKOFF_ROUND_LIMIT = 20;
|
|
24
|
+
const COLLABORATION_LATE_SEND_GRACE_MS = 5_000;
|
|
25
|
+
const ORDINARY_HUMAN_EVENT_KINDS = new Set(['', 'message']);
|
|
7
26
|
async function ensurePrivateStateRoot(path) {
|
|
8
27
|
await mkdir(path, { recursive: true, mode: PRIVATE_STATE_ROOT_MODE });
|
|
9
28
|
await chmod(path, PRIVATE_STATE_ROOT_MODE);
|
|
10
29
|
}
|
|
30
|
+
function matchesProtocolStateIdentity(left, right) {
|
|
31
|
+
return (left.anchorMessageId === right.anchorMessageId &&
|
|
32
|
+
left.issuerId === right.issuerId &&
|
|
33
|
+
left.participantIds[0] === right.participantIds[0] &&
|
|
34
|
+
left.participantIds[1] === right.participantIds[1]);
|
|
35
|
+
}
|
|
36
|
+
function shouldAdoptPeerProtocolState(current, candidate, selfAgentId) {
|
|
37
|
+
if (!matchesProtocolStateIdentity(current, candidate)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (candidate.currentTurnIndex !== current.currentTurnIndex) {
|
|
41
|
+
return candidate.currentTurnIndex > current.currentTurnIndex;
|
|
42
|
+
}
|
|
43
|
+
if ((candidate.finished && !current.finished) || (!candidate.active && current.active)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (!current.lastProtocolMessageId && candidate.lastProtocolMessageId) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (current.expectedNextSpeakerId !== selfAgentId &&
|
|
50
|
+
candidate.expectedNextSpeakerId === selfAgentId) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
async function writePrivateJsonFile(path, value, options) {
|
|
56
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
57
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
58
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
59
|
+
await writeFile(temporaryPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
60
|
+
try {
|
|
61
|
+
if (options?.exclusive) {
|
|
62
|
+
await link(temporaryPath, path);
|
|
63
|
+
await unlink(temporaryPath);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await rename(temporaryPath, path);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
await unlink(temporaryPath).catch(() => { });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class DefaultHostRuntime {
|
|
74
|
+
config;
|
|
75
|
+
provider;
|
|
76
|
+
controlPlane;
|
|
77
|
+
ensureStateRoot;
|
|
78
|
+
gateway;
|
|
79
|
+
authorizationAuditSink;
|
|
80
|
+
logger;
|
|
81
|
+
constructor(config, deps) {
|
|
82
|
+
this.config = config;
|
|
83
|
+
this.logger = deps.logger;
|
|
84
|
+
const compatibilityGates = resolveInternalCompatibilityGates();
|
|
85
|
+
const contextInjectionGateEnabled = compatibilityGates.has(CONTEXT_INJECTION_COMPATIBILITY_GATE);
|
|
86
|
+
const skillRuntimeGateEnabled = compatibilityGates.has(SKILL_RUNTIME_COMPATIBILITY_GATE);
|
|
87
|
+
const localhostGatewayGateEnabled = contextInjectionGateEnabled &&
|
|
88
|
+
skillRuntimeGateEnabled &&
|
|
89
|
+
compatibilityGates.has(LOCALHOST_GATEWAY_COMPATIBILITY_GATE);
|
|
90
|
+
const tokenBindingGateEnabled = compatibilityGates.has(TOKEN_BINDING_COMPATIBILITY_GATE);
|
|
91
|
+
const policyAuditGateEnabled = compatibilityGates.has(POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE);
|
|
92
|
+
const policyMode = resolveInternalPolicyMode(policyAuditGateEnabled);
|
|
93
|
+
const collaborationEnabled = localhostGatewayGateEnabled &&
|
|
94
|
+
compatibilityGates.has(COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE) &&
|
|
95
|
+
policyAuditGateEnabled &&
|
|
96
|
+
policyMode === 'enforce';
|
|
97
|
+
this.authorizationAuditSink = policyAuditGateEnabled
|
|
98
|
+
? new AuthorizationAuditSink({
|
|
99
|
+
stateRootDir: config.stateRootDir,
|
|
100
|
+
logger: deps.logger,
|
|
101
|
+
})
|
|
102
|
+
: undefined;
|
|
103
|
+
this.controlPlane =
|
|
104
|
+
deps.borgee ??
|
|
105
|
+
new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
|
|
106
|
+
cursorStore: createDurableCursorStore({
|
|
107
|
+
stateRootDir: config.stateRootDir,
|
|
108
|
+
}),
|
|
109
|
+
pluginId: `agents-host:${config.agent.provider}`,
|
|
110
|
+
});
|
|
111
|
+
this.gateway = createLocalhostGatewayController({
|
|
112
|
+
gateEnabled: localhostGatewayGateEnabled,
|
|
113
|
+
collaborationEnabled,
|
|
114
|
+
tokenBindingGateEnabled,
|
|
115
|
+
policyAuditGateEnabled,
|
|
116
|
+
policyMode,
|
|
117
|
+
controlPlane: this.controlPlane,
|
|
118
|
+
stateRootDir: config.stateRootDir,
|
|
119
|
+
resolveStableAgentId: deps.resolveStableAgentId,
|
|
120
|
+
logger: deps.logger,
|
|
121
|
+
auditSink: this.authorizationAuditSink,
|
|
122
|
+
authorizeCollaborationSend: deps.authorizeCollaborationSend,
|
|
123
|
+
readCollaborationDraft: deps.readCollaborationDraft,
|
|
124
|
+
});
|
|
125
|
+
this.provider =
|
|
126
|
+
deps.provider ??
|
|
127
|
+
createProvider({
|
|
128
|
+
provider: config.agent.provider,
|
|
129
|
+
stateRootDir: config.stateRootDir,
|
|
130
|
+
resolveStableAgentId: deps.resolveStableAgentId,
|
|
131
|
+
claudeCommand: config.claudeCommand,
|
|
132
|
+
claudeArgs: config.claudeArgs,
|
|
133
|
+
codexCommand: config.codexCommand,
|
|
134
|
+
codexArgs: config.codexArgs,
|
|
135
|
+
copilotCommand: config.copilotCommand,
|
|
136
|
+
copilotArgs: config.copilotArgs,
|
|
137
|
+
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
138
|
+
}, deps.logger, {
|
|
139
|
+
compatibilityGates,
|
|
140
|
+
authorizationAuditSink: this.authorizationAuditSink,
|
|
141
|
+
localhostGateway: localhostGatewayGateEnabled ? this.gateway.contextPublisher : undefined,
|
|
142
|
+
});
|
|
143
|
+
this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
|
|
144
|
+
}
|
|
145
|
+
async start(onMessage) {
|
|
146
|
+
await this.ensureStateRoot(this.config.stateRootDir);
|
|
147
|
+
await this.gateway.start();
|
|
148
|
+
try {
|
|
149
|
+
await this.controlPlane.connect(onMessage);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
await this.gateway.stop().catch(() => { });
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async stop() {
|
|
157
|
+
let thrown;
|
|
158
|
+
try {
|
|
159
|
+
await this.controlPlane.close();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
thrown = error;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await this.gateway.stop();
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
thrown ??= error;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
await this.provider.dispose?.();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
thrown ??= error;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
await this.authorizationAuditSink?.drain();
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
this.logger.error('authorization audit drain failed during shutdown', {
|
|
181
|
+
error: summarizeError(error),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (thrown) {
|
|
185
|
+
throw thrown;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function createDefaultHostRuntime(config, deps) {
|
|
190
|
+
return new DefaultHostRuntime(config, deps);
|
|
191
|
+
}
|
|
11
192
|
function hasVisibleText(value) {
|
|
12
193
|
return value.trim().length > 0;
|
|
13
194
|
}
|
|
195
|
+
function extractDispatchMentionIds(content) {
|
|
196
|
+
return [
|
|
197
|
+
...content.matchAll(/(?:^|[^A-Za-z0-9_])@([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9A-HJKMNP-TV-Z]{26})/gu),
|
|
198
|
+
]
|
|
199
|
+
.map((match) => match[1]?.trim())
|
|
200
|
+
.filter((value) => Boolean(value));
|
|
201
|
+
}
|
|
202
|
+
function hasCanonicalBodyMention(content, participantId) {
|
|
203
|
+
return extractCanonicalMentionIds(content).includes(participantId);
|
|
204
|
+
}
|
|
205
|
+
function hasRelayBodyMention(content, participantId) {
|
|
206
|
+
return (hasCanonicalBodyMention(content, participantId) ||
|
|
207
|
+
extractDispatchMentionIds(content).includes(participantId));
|
|
208
|
+
}
|
|
209
|
+
function encodeStatePathSegment(value) {
|
|
210
|
+
const encoded = Buffer.from(value, 'utf8').toString('hex');
|
|
211
|
+
return encoded.length > 0 ? encoded : 'empty';
|
|
212
|
+
}
|
|
213
|
+
function decodeStatePathSegment(value) {
|
|
214
|
+
if (value === 'empty') {
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
if (!/^(?:[0-9a-f]{2})+$/u.test(value)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return Buffer.from(value, 'hex').toString('utf8');
|
|
221
|
+
}
|
|
222
|
+
function summarizeAnchorContent(content) {
|
|
223
|
+
const collapsed = content.replace(/\s+/gu, ' ').trim();
|
|
224
|
+
if (collapsed.length <= 160) {
|
|
225
|
+
return collapsed;
|
|
226
|
+
}
|
|
227
|
+
return `${collapsed.slice(0, 157)}...`;
|
|
228
|
+
}
|
|
229
|
+
function nextProtocolFallbackDeadline(now = Date.now()) {
|
|
230
|
+
return now + PROTOCOL_PROVIDER_TURN_TIMEOUT_MS;
|
|
231
|
+
}
|
|
232
|
+
function nextProtocolKickoffDecisionDeadline(now = Date.now()) {
|
|
233
|
+
return now + PROTOCOL_KICKOFF_DECISION_TIMEOUT_MS;
|
|
234
|
+
}
|
|
235
|
+
function delay(ms) {
|
|
236
|
+
return new Promise((resolveDelay) => {
|
|
237
|
+
setTimeout(resolveDelay, ms);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function isReplyTriggeringEventType(eventType) {
|
|
241
|
+
if (eventType == null) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
const normalized = eventType.trim();
|
|
245
|
+
// The SDK forwards lowercase inbound kind literals into ChannelMessageEvent.type.
|
|
246
|
+
return normalized === '' || normalized === 'message' || normalized === 'mention';
|
|
247
|
+
}
|
|
248
|
+
function resolvePublicReplyText(reply) {
|
|
249
|
+
if (reply.controlMalformed) {
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
if (hasVisibleText(reply.text)) {
|
|
253
|
+
return reply.text;
|
|
254
|
+
}
|
|
255
|
+
return reply.awaitingUser?.question ?? '';
|
|
256
|
+
}
|
|
14
257
|
function providerUsesDraftProgress(provider) {
|
|
15
|
-
return provider === 'claude' || provider === 'copilot';
|
|
258
|
+
return provider === 'claude' || provider === 'codex' || provider === 'copilot';
|
|
16
259
|
}
|
|
17
260
|
class DraftMessageController {
|
|
18
261
|
borgee;
|
|
19
262
|
channelId;
|
|
20
263
|
canWrite;
|
|
264
|
+
logger;
|
|
265
|
+
creationMetadata;
|
|
21
266
|
messageId = null;
|
|
22
267
|
currentBody = '';
|
|
23
268
|
pendingBody = null;
|
|
24
269
|
timer = null;
|
|
25
270
|
sealed = false;
|
|
26
271
|
writeChain = Promise.resolve();
|
|
27
|
-
constructor(borgee, channelId, canWrite) {
|
|
272
|
+
constructor(borgee, channelId, canWrite, logger, creationMetadata) {
|
|
28
273
|
this.borgee = borgee;
|
|
29
274
|
this.channelId = channelId;
|
|
30
275
|
this.canWrite = canWrite;
|
|
276
|
+
this.logger = logger;
|
|
277
|
+
this.creationMetadata = creationMetadata;
|
|
31
278
|
}
|
|
32
279
|
update(text) {
|
|
33
280
|
if (this.sealed || !hasVisibleText(text)) {
|
|
@@ -40,7 +287,7 @@ class DraftMessageController {
|
|
|
40
287
|
this.timer = setTimeout(() => {
|
|
41
288
|
this.timer = null;
|
|
42
289
|
void this.flushPending().catch((error) => {
|
|
43
|
-
|
|
290
|
+
this.logger.error('failed to flush draft progress', error);
|
|
44
291
|
});
|
|
45
292
|
}, STREAM_PROGRESS_EDIT_THROTTLE_MS);
|
|
46
293
|
}
|
|
@@ -60,7 +307,7 @@ class DraftMessageController {
|
|
|
60
307
|
if (hasVisibleText(finalText)) {
|
|
61
308
|
this.pendingBody = finalText;
|
|
62
309
|
await this.flushPending();
|
|
63
|
-
return;
|
|
310
|
+
return this.messageId;
|
|
64
311
|
}
|
|
65
312
|
await this.enqueueWrite(async () => {
|
|
66
313
|
if (!this.canWrite() || !this.messageId) {
|
|
@@ -71,6 +318,7 @@ class DraftMessageController {
|
|
|
71
318
|
this.currentBody = '';
|
|
72
319
|
this.pendingBody = null;
|
|
73
320
|
});
|
|
321
|
+
return null;
|
|
74
322
|
}
|
|
75
323
|
async discard() {
|
|
76
324
|
this.sealed = true;
|
|
@@ -80,7 +328,7 @@ class DraftMessageController {
|
|
|
80
328
|
}
|
|
81
329
|
this.pendingBody = null;
|
|
82
330
|
await this.enqueueWrite(async () => {
|
|
83
|
-
if (!this.
|
|
331
|
+
if (!this.messageId) {
|
|
84
332
|
return;
|
|
85
333
|
}
|
|
86
334
|
await this.borgee.deleteMessage(this.messageId);
|
|
@@ -98,7 +346,11 @@ class DraftMessageController {
|
|
|
98
346
|
await this.borgee.editMessage(this.messageId, body);
|
|
99
347
|
}
|
|
100
348
|
else {
|
|
101
|
-
const posted = await this.borgee.postMessage(
|
|
349
|
+
const posted = await this.borgee.postMessage({
|
|
350
|
+
channelId: this.channelId,
|
|
351
|
+
body,
|
|
352
|
+
replyToId: this.creationMetadata?.replyToId,
|
|
353
|
+
});
|
|
102
354
|
this.messageId = posted.messageId;
|
|
103
355
|
}
|
|
104
356
|
this.currentBody = body;
|
|
@@ -109,201 +361,2520 @@ class DraftMessageController {
|
|
|
109
361
|
return this.writeChain;
|
|
110
362
|
}
|
|
111
363
|
}
|
|
364
|
+
const BLOCKED_AUTHOR_REFRESH_BACKOFF_MS = 1_000;
|
|
365
|
+
const MAX_QUEUED_MESSAGES_PER_CHANNEL = 8;
|
|
366
|
+
const COLLABORATION_SENDS_PER_TURN = 1;
|
|
112
367
|
/**
|
|
113
368
|
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
114
369
|
* to exactly one Borgee agent over `@borgee/plugin-sdk` (BPP / `/ws/plugin`).
|
|
115
|
-
*
|
|
116
|
-
* Conversation memory is handled entirely by each provider's native
|
|
117
|
-
* per-channel CLI session (Claude `--resume`, Copilot `--session-id`) — see
|
|
118
|
-
* the cli-client.ts file under each provider's folder. This class does not
|
|
119
|
-
* keep any message history itself.
|
|
120
|
-
*
|
|
121
|
-
* Out of scope for this single-agent runner (see README): execution/remote-command
|
|
122
|
-
* dispatch, node provisioning, systemd install, and scheduled (periodic)
|
|
123
|
-
* prompts. Multi-agent local hosting is handled one level up by the local-config
|
|
124
|
-
* supervisor, which creates one isolated `AgentsHost` per effective agent key.
|
|
125
|
-
*
|
|
126
|
-
* Message gating (mention-only / DM rules) is NOT done here: the server
|
|
127
|
-
* enforces per-agent + per-channel `require_mention` at BPP fan-out, so this
|
|
128
|
-
* host simply replies to every message it is handed (minus its own).
|
|
129
370
|
*/
|
|
130
371
|
export class AgentsHost {
|
|
131
372
|
config;
|
|
373
|
+
runtime;
|
|
132
374
|
provider;
|
|
133
375
|
borgee;
|
|
376
|
+
logger;
|
|
377
|
+
contextInjectionEnabled;
|
|
378
|
+
collaborationEnabled;
|
|
379
|
+
collaborationOutcomeModelEnabled;
|
|
380
|
+
attentionFollowSemanticsEnabled;
|
|
381
|
+
taskThreadCollaborationContractEnabled;
|
|
382
|
+
collaborationCapabilitiesDiagnosticsEnabled;
|
|
134
383
|
activeTurns = new Set();
|
|
384
|
+
awaitingUserByChannel = new Map();
|
|
385
|
+
collaborationOutcomeByChannel = new Map();
|
|
386
|
+
attentionSnapshotByChannel = new Map();
|
|
387
|
+
missedCollaborationDiagnosticByChannel = new Map();
|
|
135
388
|
progressChannelQueues = new Map();
|
|
136
389
|
progressDrafts = new Map();
|
|
390
|
+
collaborationDrafts = new Map();
|
|
391
|
+
collaborationChannels = new Map();
|
|
392
|
+
participantDirectoryByUserId = new Map();
|
|
393
|
+
participantKindsByUserId = new Map();
|
|
394
|
+
participantRefreshBackoffByUserId = new Map();
|
|
395
|
+
participantRefreshPromise = null;
|
|
396
|
+
participantDirectorySupported = true;
|
|
137
397
|
selfAgentId = null;
|
|
398
|
+
selfAgentProfile = null;
|
|
138
399
|
selfAgentIdPromise = null;
|
|
139
400
|
started = false;
|
|
140
401
|
controlPlaneClosed = false;
|
|
141
402
|
constructor(config, deps) {
|
|
142
403
|
this.config = config;
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
404
|
+
this.logger = new HostLogger(deps?.runtimeOptions);
|
|
405
|
+
const compatibilityGates = resolveInternalCompatibilityGates();
|
|
406
|
+
const contextInjectionGateEnabled = compatibilityGates.has(CONTEXT_INJECTION_COMPATIBILITY_GATE);
|
|
407
|
+
this.contextInjectionEnabled = contextInjectionGateEnabled;
|
|
408
|
+
const skillRuntimeGateEnabled = compatibilityGates.has(SKILL_RUNTIME_COMPATIBILITY_GATE);
|
|
409
|
+
const localhostGatewayGateEnabled = contextInjectionGateEnabled &&
|
|
410
|
+
skillRuntimeGateEnabled &&
|
|
411
|
+
compatibilityGates.has(LOCALHOST_GATEWAY_COMPATIBILITY_GATE);
|
|
412
|
+
const policyAuditGateEnabled = compatibilityGates.has(POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE);
|
|
413
|
+
this.collaborationEnabled =
|
|
414
|
+
localhostGatewayGateEnabled &&
|
|
415
|
+
compatibilityGates.has(COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE) &&
|
|
416
|
+
policyAuditGateEnabled &&
|
|
417
|
+
resolveInternalPolicyMode(policyAuditGateEnabled) === 'enforce';
|
|
418
|
+
this.collaborationOutcomeModelEnabled = compatibilityGates.has(COLLABORATION_OUTCOME_MODEL_COMPATIBILITY_GATE);
|
|
419
|
+
this.attentionFollowSemanticsEnabled = compatibilityGates.has(ATTENTION_FOLLOW_SEMANTICS_COMPATIBILITY_GATE);
|
|
420
|
+
this.taskThreadCollaborationContractEnabled =
|
|
421
|
+
contextInjectionGateEnabled
|
|
422
|
+
&& compatibilityGates.has(TASK_THREAD_COLLABORATION_CONTRACT_COMPATIBILITY_GATE);
|
|
423
|
+
this.collaborationCapabilitiesDiagnosticsEnabled =
|
|
424
|
+
contextInjectionGateEnabled
|
|
425
|
+
&& compatibilityGates.has(COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE);
|
|
426
|
+
this.runtime = createDefaultHostRuntime(config, {
|
|
427
|
+
logger: this.logger,
|
|
428
|
+
provider: deps?.provider,
|
|
429
|
+
borgee: deps?.borgee,
|
|
146
430
|
resolveStableAgentId: () => this.selfAgentId ?? undefined,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
copilotCommand: config.copilotCommand,
|
|
150
|
-
copilotArgs: config.copilotArgs,
|
|
151
|
-
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
152
|
-
});
|
|
153
|
-
this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
|
|
154
|
-
cursorStore: createDurableCursorStore({
|
|
155
|
-
stateRootDir: config.stateRootDir,
|
|
156
|
-
}),
|
|
157
|
-
// Lets the server know which runtime/provider actually connected
|
|
158
|
-
// (internal/bpp ConnectHandler → users.last_connected_plugin_id), so
|
|
159
|
-
// the web UI can show real state instead of a client-side guess.
|
|
160
|
-
pluginId: `agents-host:${config.agent.provider}`,
|
|
431
|
+
readCollaborationDraft: (input) => this.readCollaborationDraft(input),
|
|
432
|
+
authorizeCollaborationSend: (input) => this.authorizeCollaborationSend(input),
|
|
161
433
|
});
|
|
434
|
+
this.provider = this.runtime.provider;
|
|
435
|
+
this.borgee = this.runtime.controlPlane;
|
|
162
436
|
}
|
|
163
437
|
async start() {
|
|
164
438
|
if (this.started)
|
|
165
439
|
return;
|
|
166
440
|
this.started = true;
|
|
167
441
|
this.controlPlaneClosed = false;
|
|
168
|
-
|
|
169
|
-
await this.borgee.connect((message) => {
|
|
170
|
-
if (!this.started) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const task = providerUsesDraftProgress(this.config.agent.provider)
|
|
174
|
-
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleMessage(message))
|
|
175
|
-
: this.handleMessage(message);
|
|
176
|
-
this.trackActiveTurn(task);
|
|
177
|
-
});
|
|
178
|
-
const agentId = await this.ensureSelfAgentId();
|
|
179
|
-
console.log('[agents-host] connected', {
|
|
180
|
-
agentId,
|
|
181
|
-
agentName: this.config.agent.agentName,
|
|
442
|
+
this.logger.debug('starting host', {
|
|
182
443
|
provider: this.config.agent.provider,
|
|
444
|
+
agentName: this.config.agent.agentName,
|
|
445
|
+
stateRootDir: this.config.stateRootDir,
|
|
446
|
+
collaborationEnabled: this.collaborationEnabled,
|
|
183
447
|
});
|
|
448
|
+
const bufferedMessages = [];
|
|
449
|
+
let bufferingMessages = true;
|
|
450
|
+
try {
|
|
451
|
+
await this.runtime.start((message) => {
|
|
452
|
+
if (!this.started) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (bufferingMessages) {
|
|
456
|
+
bufferedMessages.push(message);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.dispatchMessage(message);
|
|
460
|
+
});
|
|
461
|
+
const agentId = await this.ensureSelfAgentId();
|
|
462
|
+
bufferingMessages = false;
|
|
463
|
+
while (bufferedMessages.length > 0) {
|
|
464
|
+
const pendingMessages = bufferedMessages.splice(0, bufferedMessages.length);
|
|
465
|
+
for (const message of pendingMessages) {
|
|
466
|
+
this.dispatchMessage(message);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
this.logger.debug('connected host', {
|
|
470
|
+
agentId,
|
|
471
|
+
agentName: this.config.agent.agentName,
|
|
472
|
+
provider: this.config.agent.provider,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
bufferingMessages = false;
|
|
477
|
+
bufferedMessages.length = 0;
|
|
478
|
+
this.started = false;
|
|
479
|
+
this.controlPlaneClosed = true;
|
|
480
|
+
await this.runtime.stop().catch(() => { });
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
184
483
|
}
|
|
185
484
|
async stop() {
|
|
186
485
|
if (!this.started)
|
|
187
486
|
return;
|
|
188
487
|
this.started = false;
|
|
488
|
+
this.logger.debug('stopping host', {
|
|
489
|
+
activeTurnCount: this.activeTurns.size,
|
|
490
|
+
provider: this.config.agent.provider,
|
|
491
|
+
agentName: this.config.agent.agentName,
|
|
492
|
+
});
|
|
493
|
+
for (const channelId of this.collaborationChannels.keys()) {
|
|
494
|
+
this.invalidateChannelTurn(channelId);
|
|
495
|
+
this.clearProtocolFallback(this.collaborationChannels.get(channelId));
|
|
496
|
+
}
|
|
189
497
|
for (const draft of this.progressDrafts.values()) {
|
|
190
498
|
draft.cancelProgress();
|
|
191
499
|
}
|
|
500
|
+
for (const channelId of [...this.collaborationDrafts.keys()]) {
|
|
501
|
+
this.clearCollaborationDraft(channelId);
|
|
502
|
+
}
|
|
503
|
+
this.awaitingUserByChannel.clear();
|
|
504
|
+
this.collaborationOutcomeByChannel.clear();
|
|
505
|
+
this.attentionSnapshotByChannel.clear();
|
|
506
|
+
this.missedCollaborationDiagnosticByChannel.clear();
|
|
192
507
|
this.controlPlaneClosed = true;
|
|
193
|
-
await this.
|
|
194
|
-
await this.provider.dispose?.();
|
|
508
|
+
await this.runtime.stop();
|
|
195
509
|
await Promise.allSettled([...this.activeTurns]);
|
|
196
510
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
agentName: this.config.agent.agentName,
|
|
202
|
-
channelId: msg.channel_id,
|
|
511
|
+
trackActiveTurn(task) {
|
|
512
|
+
this.activeTurns.add(task);
|
|
513
|
+
void task.finally(() => {
|
|
514
|
+
this.activeTurns.delete(task);
|
|
203
515
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
516
|
+
}
|
|
517
|
+
dispatchMessage(message) {
|
|
518
|
+
const task = this.collaborationEnabled
|
|
519
|
+
? this.handleCollaborationMessage(message)
|
|
520
|
+
: providerUsesDraftProgress(this.config.agent.provider)
|
|
521
|
+
? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleLegacyMessage(message))
|
|
522
|
+
: this.handleLegacyMessage(message);
|
|
523
|
+
this.trackActiveTurn(task);
|
|
524
|
+
}
|
|
525
|
+
async handleLegacyMessage(msg) {
|
|
526
|
+
const normalized = await this.normalizeMessage(msg);
|
|
527
|
+
if (!normalized) {
|
|
207
528
|
return;
|
|
208
529
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
provider: this.config.agent.provider,
|
|
222
|
-
channelId: msg.channel_id,
|
|
223
|
-
incomingAuthorId: authorId,
|
|
224
|
-
incomingContent: content,
|
|
225
|
-
}, useDraftProgress
|
|
226
|
-
? {
|
|
227
|
-
onProgress: (update) => {
|
|
228
|
-
if (!this.started || this.controlPlaneClosed) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
this.ensureProgressDraft(msg.channel_id).update(update.text);
|
|
232
|
-
},
|
|
530
|
+
let taskThreadContext = { state: 'inactive' };
|
|
531
|
+
let attentionSnapshot;
|
|
532
|
+
if (this.attentionFollowSemanticsEnabled
|
|
533
|
+
|| this.taskThreadCollaborationContractEnabled) {
|
|
534
|
+
taskThreadContext = await this.buildTurnTaskThreadContext(msg, msg.channel_id);
|
|
535
|
+
if (this.attentionFollowSemanticsEnabled) {
|
|
536
|
+
const channelState = this.ensureCollaborationChannelState(msg.channel_id);
|
|
537
|
+
const authorKind = await this.classifyAuthor(normalized.authorId, false);
|
|
538
|
+
await this.ensureAttentionSnapshotLoaded(msg.channel_id, channelState);
|
|
539
|
+
attentionSnapshot = this.getAttentionSnapshotForTurn(msg.channel_id, taskThreadContext);
|
|
540
|
+
if (this.isHumanWakeFiltered(msg, authorKind, attentionSnapshot)) {
|
|
541
|
+
return;
|
|
233
542
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (msg.message_type === 'task_assignment') {
|
|
546
|
+
await this.markAssignedTaskInProgress(msg.channel_id, normalized.content);
|
|
547
|
+
}
|
|
548
|
+
await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot);
|
|
549
|
+
}
|
|
550
|
+
async handleCollaborationMessage(msg) {
|
|
551
|
+
const normalized = await this.normalizeMessage(msg);
|
|
552
|
+
if (!normalized) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (msg.message_type === 'task_assignment') {
|
|
556
|
+
await this.markAssignedTaskInProgress(msg.channel_id, normalized.content);
|
|
557
|
+
}
|
|
558
|
+
const channelState = this.ensureCollaborationChannelState(msg.channel_id);
|
|
559
|
+
await this.ensureProtocolStateLoaded(msg.channel_id, channelState);
|
|
560
|
+
await this.ensureAttentionSnapshotLoaded(msg.channel_id, channelState);
|
|
561
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
562
|
+
const authorKind = await this.classifyAuthor(normalized.authorId, false);
|
|
563
|
+
const taskThreadContext = await this.buildTurnTaskThreadContext(msg, msg.channel_id);
|
|
564
|
+
const attentionSnapshot = this.getAttentionSnapshotForTurn(msg.channel_id, taskThreadContext);
|
|
565
|
+
if (channelState.protocol?.active) {
|
|
566
|
+
if (msg.message_id && authorKind === 'agent') {
|
|
567
|
+
await this.handleActiveProtocolParticipantMessage(msg, normalized, channelState, channelState.protocol, selfAgentId);
|
|
568
|
+
if (channelState.protocol?.active ||
|
|
569
|
+
channelState.queue.some((message) => message.kind === 'protocol-turn')) {
|
|
570
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
240
571
|
return;
|
|
241
572
|
}
|
|
242
|
-
await (draft ?? this.ensureProgressDraft(msg.channel_id)).finalize(reply.text);
|
|
243
|
-
this.progressDrafts.delete(msg.channel_id);
|
|
244
573
|
}
|
|
245
|
-
|
|
246
|
-
|
|
574
|
+
if (authorKind !== 'agent') {
|
|
575
|
+
if (msg.message_id === channelState.protocol.anchorMessageId) {
|
|
576
|
+
await this.handleActiveProtocolAnchorReplay(msg, normalized, channelState, channelState.protocol, selfAgentId);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
this.cancelProtocolState(msg.channel_id, channelState);
|
|
247
580
|
}
|
|
248
581
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
582
|
+
if (channelState.blocked) {
|
|
583
|
+
const blockedAuthorKind = await this.classifyBlockedWakeAuthor(normalized.authorId, channelState.blocked);
|
|
584
|
+
if (blockedAuthorKind === 'agent') {
|
|
585
|
+
channelState.blocked.suppressedAgentMessages += 1;
|
|
586
|
+
this.recordBlockedSuppressionOutcome(msg.channel_id, channelState);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (blockedAuthorKind === 'unknown') {
|
|
590
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
591
|
+
raw: msg,
|
|
592
|
+
authorId: normalized.authorId,
|
|
593
|
+
authorKind: 'unknown',
|
|
594
|
+
content: normalized.content,
|
|
595
|
+
attentionSnapshot,
|
|
596
|
+
taskThreadContext,
|
|
597
|
+
});
|
|
598
|
+
const resumed = await this.tryResumeBlockedFromQueue(msg.channel_id, channelState);
|
|
599
|
+
if (resumed) {
|
|
600
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
255
601
|
}
|
|
602
|
+
return;
|
|
256
603
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
604
|
+
const wakeBlockedState = channelState.blocked.awaitingUser;
|
|
605
|
+
channelState.blocked = undefined;
|
|
606
|
+
this.awaitingUserByChannel.delete(msg.channel_id);
|
|
607
|
+
channelState.queue = [
|
|
608
|
+
{
|
|
609
|
+
raw: msg,
|
|
610
|
+
authorId: normalized.authorId,
|
|
611
|
+
authorKind: 'human',
|
|
612
|
+
content: normalized.content,
|
|
613
|
+
attentionSnapshot,
|
|
614
|
+
taskThreadContext,
|
|
615
|
+
wakeBlockedState,
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
619
|
+
return;
|
|
262
620
|
}
|
|
263
|
-
|
|
264
|
-
|
|
621
|
+
if (authorKind !== 'agent') {
|
|
622
|
+
const protocolCandidate = this.detectProtocolKickoffCandidate(msg.message_id, normalized.content, selfAgentId);
|
|
623
|
+
if (protocolCandidate) {
|
|
624
|
+
const protocolRequest = await this.resolveProtocolKickoffRequest(msg, normalized, protocolCandidate, selfAgentId);
|
|
625
|
+
if (protocolRequest) {
|
|
626
|
+
await this.startProtocolForAnchor(msg, normalized, channelState, protocolRequest, selfAgentId);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
265
630
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.
|
|
631
|
+
if (this.isHumanWakeFiltered(msg, authorKind, attentionSnapshot)) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (authorKind !== 'agent' && channelState.activeTurn) {
|
|
635
|
+
this.supersedeActiveTurn(msg.channel_id, channelState.activeTurn);
|
|
636
|
+
}
|
|
637
|
+
if (authorKind !== 'agent') {
|
|
638
|
+
channelState.recentTurn = undefined;
|
|
639
|
+
}
|
|
640
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
641
|
+
raw: msg,
|
|
642
|
+
authorId: normalized.authorId,
|
|
643
|
+
authorKind,
|
|
644
|
+
content: normalized.content,
|
|
645
|
+
attentionSnapshot,
|
|
646
|
+
taskThreadContext,
|
|
271
647
|
});
|
|
648
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
272
649
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
650
|
+
detectProtocolKickoffCandidate(anchorMessageId, content, selfAgentId) {
|
|
651
|
+
if (!anchorMessageId) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
const mentions = extractCanonicalMentionIds(content);
|
|
655
|
+
if (mentions.length !== 2) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const unique = [...new Set(mentions)].sort();
|
|
659
|
+
if (unique.length !== 2 || !unique.includes(selfAgentId)) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
anchorMessageId,
|
|
664
|
+
participantIds: [unique[0], unique[1]],
|
|
665
|
+
issuerId: unique[0],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async validateProtocolKickoffCandidate(anchorAuthorId, protocolCandidate, selfAgentId) {
|
|
669
|
+
const anchorAuthorKind = await this.classifyAuthor(anchorAuthorId, true);
|
|
670
|
+
if (anchorAuthorKind === 'agent') {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
if (protocolCandidate.participantIds.includes(anchorAuthorId)) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
for (const participantId of protocolCandidate.participantIds) {
|
|
677
|
+
if (participantId === selfAgentId) {
|
|
678
|
+
continue;
|
|
281
679
|
}
|
|
680
|
+
const participantKind = await this.classifyAuthor(participantId, true);
|
|
681
|
+
if (participantKind === 'human') {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return protocolCandidate;
|
|
686
|
+
}
|
|
687
|
+
async resolveProtocolKickoffRequest(msg, normalized, protocolCandidate, selfAgentId) {
|
|
688
|
+
const validatedCandidate = await this.validateProtocolKickoffCandidate(normalized.authorId, protocolCandidate, selfAgentId);
|
|
689
|
+
if (!validatedCandidate) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
const pendingDecision = await this.ensureProtocolKickoffDecisionRecord(msg.channel_id, validatedCandidate);
|
|
693
|
+
if (!pendingDecision) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const decision = pendingDecision.status === 'pending'
|
|
697
|
+
? validatedCandidate.issuerId === selfAgentId
|
|
698
|
+
? await this.evaluateIssuerProtocolKickoff(msg, normalized, validatedCandidate, pendingDecision)
|
|
699
|
+
: await this.waitForProtocolKickoffDecision(msg.channel_id, validatedCandidate, pendingDecision.deadlineAt)
|
|
700
|
+
: pendingDecision;
|
|
701
|
+
if (!decision || decision.status !== 'start') {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const rounds = this.validateProtocolKickoffRounds(decision.rounds);
|
|
705
|
+
if (!rounds) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
participantIds: decision.participantIds,
|
|
710
|
+
issuerId: decision.issuerId,
|
|
711
|
+
maxTurnCount: rounds * 2,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
validateProtocolKickoffRounds(rounds) {
|
|
715
|
+
if (!Number.isFinite(rounds) ||
|
|
716
|
+
typeof rounds !== 'number' ||
|
|
717
|
+
!Number.isInteger(rounds) ||
|
|
718
|
+
rounds < 1) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
return Math.min(rounds, PROTOCOL_KICKOFF_ROUND_LIMIT);
|
|
722
|
+
}
|
|
723
|
+
protocolRequestFromKickoffDecision(decision) {
|
|
724
|
+
const rounds = this.validateProtocolKickoffRounds(decision.rounds);
|
|
725
|
+
if (!rounds) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
participantIds: decision.participantIds,
|
|
730
|
+
issuerId: decision.issuerId,
|
|
731
|
+
maxTurnCount: rounds * 2,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async evaluateIssuerProtocolKickoff(msg, normalized, protocolCandidate, pendingDecision) {
|
|
735
|
+
const priorMissedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(msg.channel_id);
|
|
736
|
+
const executed = await this.executeTurn(msg, normalized.authorId, normalized.content, {
|
|
737
|
+
kickoff: protocolCandidate,
|
|
738
|
+
silent: true,
|
|
282
739
|
});
|
|
283
|
-
|
|
284
|
-
|
|
740
|
+
if (executed.stale) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
this.recordCollaborationOutcome(msg.channel_id, {
|
|
744
|
+
responseState: executed.completed ? 'responded' : 'failed',
|
|
745
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
746
|
+
});
|
|
747
|
+
this.updateMissedCollaborationDiagnostic(msg.channel_id, priorMissedCollaborationDiagnostic);
|
|
748
|
+
const reply = executed.reply;
|
|
749
|
+
const rounds = reply?.control?.kind === 'start-protocol'
|
|
750
|
+
? this.validateProtocolKickoffRounds(reply.control.rounds)
|
|
751
|
+
: null;
|
|
752
|
+
const finalizedDecision = await this.finalizeProtocolKickoffDecision(msg.channel_id, protocolCandidate, pendingDecision.deadlineAt, {
|
|
753
|
+
status: rounds ? 'start' : 'decline',
|
|
754
|
+
...(rounds ? { rounds } : {}),
|
|
755
|
+
});
|
|
756
|
+
this.logger.debug('completed silent kickoff evaluation', {
|
|
757
|
+
channelId: msg.channel_id,
|
|
758
|
+
messageId: msg.message_id,
|
|
759
|
+
anchorMessageId: protocolCandidate.anchorMessageId,
|
|
760
|
+
provider: this.config.agent.provider,
|
|
761
|
+
agentName: this.config.agent.agentName,
|
|
762
|
+
controlKind: reply?.control?.kind ?? null,
|
|
763
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
764
|
+
outcome: rounds ? 'start' : reply?.controlMalformed ? 'malformed-control' : 'no-control',
|
|
765
|
+
persistedStatus: finalizedDecision?.status ?? null,
|
|
766
|
+
persistedRounds: finalizedDecision?.rounds ?? null,
|
|
767
|
+
replyLength: reply ? resolvePublicReplyText(reply).length : 0,
|
|
768
|
+
});
|
|
769
|
+
return finalizedDecision;
|
|
285
770
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
771
|
+
async waitForProtocolKickoffDecision(channelId, protocolCandidate, deadlineAt) {
|
|
772
|
+
for (;;) {
|
|
773
|
+
const decision = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
774
|
+
if (decision &&
|
|
775
|
+
this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, decision)) {
|
|
776
|
+
if (decision.status !== 'pending') {
|
|
777
|
+
return decision;
|
|
778
|
+
}
|
|
779
|
+
const now = Date.now();
|
|
780
|
+
if (decision.deadlineAt <= now) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
await delay(Math.min(PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS, decision.deadlineAt - now));
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const now = Date.now();
|
|
787
|
+
if (deadlineAt <= now) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
await delay(Math.min(PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS, deadlineAt - now));
|
|
291
791
|
}
|
|
292
|
-
return draft;
|
|
293
792
|
}
|
|
294
|
-
async
|
|
295
|
-
|
|
296
|
-
|
|
793
|
+
async ensureProtocolKickoffDecisionRecord(channelId, protocolCandidate) {
|
|
794
|
+
const existing = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
795
|
+
if (existing) {
|
|
796
|
+
return this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, existing)
|
|
797
|
+
? existing
|
|
798
|
+
: null;
|
|
297
799
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
800
|
+
const record = {
|
|
801
|
+
channelId,
|
|
802
|
+
...protocolCandidate,
|
|
803
|
+
deadlineAt: nextProtocolKickoffDecisionDeadline(),
|
|
804
|
+
status: 'pending',
|
|
805
|
+
};
|
|
806
|
+
try {
|
|
807
|
+
await this.writeProtocolKickoffDecisionRecord(record, 'wx');
|
|
808
|
+
return record;
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
const code = error.code;
|
|
812
|
+
if (code !== 'EEXIST') {
|
|
304
813
|
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const persisted = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
817
|
+
return persisted &&
|
|
818
|
+
this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, persisted)
|
|
819
|
+
? persisted
|
|
820
|
+
: null;
|
|
821
|
+
}
|
|
822
|
+
async finalizeProtocolKickoffDecision(channelId, protocolCandidate, deadlineAt, decision) {
|
|
823
|
+
const current = await this.readProtocolKickoffDecisionRecord(channelId, protocolCandidate.anchorMessageId);
|
|
824
|
+
if (current &&
|
|
825
|
+
!this.matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, current)) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
const effectiveDeadlineAt = current?.deadlineAt ?? deadlineAt;
|
|
829
|
+
if (effectiveDeadlineAt <= Date.now()) {
|
|
830
|
+
if (!current || current.status === 'pending') {
|
|
831
|
+
await this.writeProtocolKickoffDecisionRecord({
|
|
832
|
+
channelId,
|
|
833
|
+
...protocolCandidate,
|
|
834
|
+
deadlineAt: effectiveDeadlineAt,
|
|
835
|
+
status: 'decline',
|
|
836
|
+
decidedAt: Date.now(),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
if (current?.status === 'start') {
|
|
842
|
+
return current;
|
|
843
|
+
}
|
|
844
|
+
if (current?.status === 'decline') {
|
|
845
|
+
return current;
|
|
846
|
+
}
|
|
847
|
+
const finalized = {
|
|
848
|
+
channelId,
|
|
849
|
+
...protocolCandidate,
|
|
850
|
+
deadlineAt: effectiveDeadlineAt,
|
|
851
|
+
status: decision.status,
|
|
852
|
+
...(decision.rounds ? { rounds: decision.rounds } : {}),
|
|
853
|
+
decidedAt: Date.now(),
|
|
854
|
+
};
|
|
855
|
+
await this.writeProtocolKickoffDecisionRecord(finalized);
|
|
856
|
+
return finalized;
|
|
857
|
+
}
|
|
858
|
+
matchesProtocolKickoffDecisionRecord(channelId, protocolCandidate, record) {
|
|
859
|
+
return (record.channelId === channelId &&
|
|
860
|
+
record.anchorMessageId === protocolCandidate.anchorMessageId &&
|
|
861
|
+
record.issuerId === protocolCandidate.issuerId &&
|
|
862
|
+
record.participantIds[0] === protocolCandidate.participantIds[0] &&
|
|
863
|
+
record.participantIds[1] === protocolCandidate.participantIds[1]);
|
|
864
|
+
}
|
|
865
|
+
resolveProtocolKickoffDecisionPath(channelId, anchorMessageId) {
|
|
866
|
+
return resolveSharedProtocolKickoffDecisionPath(this.config.stateRootDir, channelId, anchorMessageId);
|
|
867
|
+
}
|
|
868
|
+
resolveSharedProtocolStatusPath(channelId, anchorMessageId) {
|
|
869
|
+
return resolveSharedProtocolStatusPath(this.config.stateRootDir, channelId, anchorMessageId);
|
|
870
|
+
}
|
|
871
|
+
async readProtocolKickoffDecisionRecord(channelId, anchorMessageId) {
|
|
872
|
+
try {
|
|
873
|
+
const content = await readFile(this.resolveProtocolKickoffDecisionPath(channelId, anchorMessageId), 'utf8');
|
|
874
|
+
return JSON.parse(content);
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
const code = error.code;
|
|
878
|
+
if (code === 'ENOENT') {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
throw error;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async writeProtocolKickoffDecisionRecord(record, flag = 'w') {
|
|
885
|
+
const path = this.resolveProtocolKickoffDecisionPath(record.channelId, record.anchorMessageId);
|
|
886
|
+
await writePrivateJsonFile(path, record, { exclusive: flag === 'wx' });
|
|
887
|
+
}
|
|
888
|
+
async handleActiveProtocolAnchorReplay(msg, normalized, channelState, protocolState, selfAgentId) {
|
|
889
|
+
if (protocolState.currentTurnIndex > 0) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (selfAgentId === protocolState.issuerId) {
|
|
893
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
894
|
+
kind: 'protocol-turn',
|
|
895
|
+
raw: msg,
|
|
896
|
+
authorId: normalized.authorId,
|
|
897
|
+
authorKind: 'human',
|
|
898
|
+
content: normalized.content,
|
|
305
899
|
});
|
|
900
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
901
|
+
return;
|
|
306
902
|
}
|
|
307
|
-
|
|
903
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
904
|
+
}
|
|
905
|
+
async startProtocolForAnchor(msg, normalized, channelState, protocolRequest, selfAgentId) {
|
|
906
|
+
this.cancelProtocolState(msg.channel_id, channelState);
|
|
907
|
+
const protocolState = {
|
|
908
|
+
ownerAgentId: selfAgentId,
|
|
909
|
+
anchorAuthorId: normalized.authorId,
|
|
910
|
+
anchorContent: normalized.content,
|
|
911
|
+
anchorMessageId: msg.message_id ?? randomUUID(),
|
|
912
|
+
participantIds: protocolRequest.participantIds,
|
|
913
|
+
issuerId: protocolRequest.issuerId,
|
|
914
|
+
expectedNextSpeakerId: protocolRequest.issuerId,
|
|
915
|
+
currentTurnIndex: 0,
|
|
916
|
+
maxTurnCount: protocolRequest.maxTurnCount,
|
|
917
|
+
active: true,
|
|
918
|
+
finished: false,
|
|
919
|
+
fallbackDeadlineAt: nextProtocolFallbackDeadline(),
|
|
920
|
+
fallbackTimer: null,
|
|
921
|
+
};
|
|
922
|
+
channelState.protocol = protocolState;
|
|
923
|
+
channelState.protocolLoaded = true;
|
|
924
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
925
|
+
await this.persistSharedProtocolStatus(msg.channel_id, protocolState);
|
|
926
|
+
if (selfAgentId === protocolRequest.issuerId) {
|
|
927
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
928
|
+
kind: 'protocol-turn',
|
|
929
|
+
raw: msg,
|
|
930
|
+
authorId: normalized.authorId,
|
|
931
|
+
authorKind: 'human',
|
|
932
|
+
content: normalized.content,
|
|
933
|
+
});
|
|
934
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
938
|
+
}
|
|
939
|
+
async deliverDeferredTurnReply(channelId, activeTurn, executed) {
|
|
940
|
+
if (!executed.completed ||
|
|
941
|
+
executed.stale ||
|
|
942
|
+
!this.canAcceptTurnOutput(channelId, activeTurn.generation)) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const publicReplyText = executed.publicReplyText ?? '';
|
|
946
|
+
if (!this.started || this.controlPlaneClosed || !hasVisibleText(publicReplyText)) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const posted = await this.borgee.postMessage({
|
|
950
|
+
channelId,
|
|
951
|
+
body: executed.visibleReplyBody ?? publicReplyText,
|
|
952
|
+
replyToId: activeTurn.delivery?.replyToId,
|
|
953
|
+
});
|
|
954
|
+
executed.deliveredMessageId = posted.messageId;
|
|
955
|
+
}
|
|
956
|
+
async handleActiveProtocolParticipantMessage(msg, normalized, channelState, protocolState, selfAgentId) {
|
|
957
|
+
if (!msg.message_id || normalized.authorId !== protocolState.expectedNextSpeakerId) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const expectedReplyToId = protocolState.lastProtocolMessageId ?? protocolState.anchorMessageId;
|
|
961
|
+
const confirmed = await this.confirmProtocolMessage(msg.channel_id, msg.message_id, normalized.authorId, expectedReplyToId, msg.created_at);
|
|
962
|
+
if (!confirmed) {
|
|
963
|
+
if (selfAgentId !== protocolState.issuerId) {
|
|
964
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
965
|
+
}
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
this.clearProtocolFallback(channelState);
|
|
969
|
+
protocolState.currentTurnIndex += 1;
|
|
970
|
+
protocolState.lastProtocolMessageId = msg.message_id;
|
|
971
|
+
const continuesProtocol = msg.type === 'mention' || hasRelayBodyMention(confirmed.body, selfAgentId);
|
|
972
|
+
if (!continuesProtocol) {
|
|
973
|
+
protocolState.active = false;
|
|
974
|
+
protocolState.finished = true;
|
|
975
|
+
protocolState.expectedNextSpeakerId = normalized.authorId;
|
|
976
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (protocolState.currentTurnIndex >= protocolState.maxTurnCount) {
|
|
980
|
+
protocolState.active = false;
|
|
981
|
+
protocolState.finished = true;
|
|
982
|
+
protocolState.expectedNextSpeakerId = normalized.authorId;
|
|
983
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
protocolState.expectedNextSpeakerId = this.otherProtocolParticipant(protocolState, normalized.authorId);
|
|
987
|
+
protocolState.fallbackDeadlineAt = nextProtocolFallbackDeadline();
|
|
988
|
+
await this.persistProtocolState(msg.channel_id, protocolState);
|
|
989
|
+
if (protocolState.expectedNextSpeakerId !== selfAgentId) {
|
|
990
|
+
if (selfAgentId !== protocolState.issuerId) {
|
|
991
|
+
this.armProtocolFallback(msg.channel_id, channelState, protocolState);
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
this.enqueueCollaborationMessage(channelState, {
|
|
996
|
+
kind: 'protocol-turn',
|
|
997
|
+
raw: msg,
|
|
998
|
+
authorId: normalized.authorId,
|
|
999
|
+
authorKind: 'agent',
|
|
1000
|
+
content: normalized.content,
|
|
1001
|
+
});
|
|
1002
|
+
await this.ensureCollaborationProcessor(msg.channel_id, channelState);
|
|
1003
|
+
}
|
|
1004
|
+
enqueueCollaborationMessage(channelState, message) {
|
|
1005
|
+
channelState.queue.push(message);
|
|
1006
|
+
while (channelState.queue.length > MAX_QUEUED_MESSAGES_PER_CHANNEL) {
|
|
1007
|
+
channelState.queue.shift();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
ensureCollaborationChannelState(channelId) {
|
|
1011
|
+
let state = this.collaborationChannels.get(channelId);
|
|
1012
|
+
if (!state) {
|
|
1013
|
+
state = {
|
|
1014
|
+
generation: 0,
|
|
1015
|
+
queue: [],
|
|
1016
|
+
running: false,
|
|
1017
|
+
protocolLoaded: false,
|
|
1018
|
+
attentionLoaded: false,
|
|
1019
|
+
};
|
|
1020
|
+
this.collaborationChannels.set(channelId, state);
|
|
1021
|
+
}
|
|
1022
|
+
return state;
|
|
1023
|
+
}
|
|
1024
|
+
async ensureCollaborationProcessor(channelId, channelState) {
|
|
1025
|
+
if (channelState.processor) {
|
|
1026
|
+
await channelState.processor;
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const processor = this.processCollaborationQueue(channelId, channelState).finally(() => {
|
|
1030
|
+
if (channelState.processor === processor) {
|
|
1031
|
+
channelState.processor = undefined;
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
channelState.processor = processor;
|
|
1035
|
+
await processor;
|
|
1036
|
+
}
|
|
1037
|
+
async processCollaborationQueue(channelId, channelState) {
|
|
1038
|
+
while (this.started && !this.controlPlaneClosed && !channelState.blocked) {
|
|
1039
|
+
const next = channelState.queue.shift();
|
|
1040
|
+
if (!next) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
channelState.running = true;
|
|
1044
|
+
try {
|
|
1045
|
+
await this.runCollaborationTurn(channelId, channelState, next);
|
|
1046
|
+
}
|
|
1047
|
+
finally {
|
|
1048
|
+
channelState.running = false;
|
|
1049
|
+
}
|
|
1050
|
+
if (channelState.blocked) {
|
|
1051
|
+
channelState.queue = channelState.queue.filter((queued) => queued.authorKind !== 'agent');
|
|
1052
|
+
const resumed = await this.tryResumeBlockedFromQueue(channelId, channelState);
|
|
1053
|
+
if (resumed) {
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async tryResumeBlockedFromQueue(channelId, channelState) {
|
|
1060
|
+
const blocked = channelState.blocked;
|
|
1061
|
+
if (!blocked || channelState.queue.length === 0) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
const retained = [];
|
|
1065
|
+
for (const [index, queued] of channelState.queue.entries()) {
|
|
1066
|
+
const classified = queued.authorKind === 'human'
|
|
1067
|
+
? 'human'
|
|
1068
|
+
: await this.classifyQueuedBlockedWakeCandidate(queued, blocked);
|
|
1069
|
+
if (classified === 'agent') {
|
|
1070
|
+
blocked.suppressedAgentMessages += 1;
|
|
1071
|
+
this.recordBlockedSuppressionOutcome(channelId, channelState);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (classified === 'unknown') {
|
|
1075
|
+
retained.push(queued);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
const wakeBlockedState = blocked.awaitingUser;
|
|
1079
|
+
channelState.blocked = undefined;
|
|
1080
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1081
|
+
channelState.queue = [
|
|
1082
|
+
{
|
|
1083
|
+
...queued,
|
|
1084
|
+
authorKind: 'human',
|
|
1085
|
+
wakeBlockedState,
|
|
1086
|
+
},
|
|
1087
|
+
...channelState.queue.slice(index + 1),
|
|
1088
|
+
];
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
channelState.queue = retained;
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
async runCollaborationTurn(channelId, channelState, message) {
|
|
1095
|
+
if (message.kind === 'protocol-turn') {
|
|
1096
|
+
await this.runProtocolManagedTurn(channelId, channelState, message);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const taskThreadContext = await this.buildTurnTaskThreadContext(message.raw, channelId);
|
|
1100
|
+
const attentionSnapshot = this.getAttentionSnapshotForTurn(channelId, taskThreadContext);
|
|
1101
|
+
if (!message.wakeBlockedState
|
|
1102
|
+
&& this.isHumanWakeFiltered(message.raw, message.authorKind, attentionSnapshot)) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
channelState.generation += 1;
|
|
1106
|
+
const activeTurn = {
|
|
1107
|
+
generation: channelState.generation,
|
|
1108
|
+
turnExecutionId: randomUUID(),
|
|
1109
|
+
superseded: false,
|
|
1110
|
+
collaborationMessagesSent: 0,
|
|
1111
|
+
targetCooldowns: new Map(),
|
|
1112
|
+
};
|
|
1113
|
+
this.clearCollaborationDraft(channelId);
|
|
1114
|
+
channelState.recentTurn = undefined;
|
|
1115
|
+
channelState.activeTurn = activeTurn;
|
|
1116
|
+
const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
|
|
1117
|
+
activeTurn,
|
|
1118
|
+
incomingAuthorKind: message.authorKind,
|
|
1119
|
+
attentionSnapshot,
|
|
1120
|
+
taskThreadContext,
|
|
1121
|
+
});
|
|
1122
|
+
channelState.activeTurn = undefined;
|
|
1123
|
+
if (executed.stale) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (executed.completed) {
|
|
1127
|
+
await this.commitAttentionSnapshot(channelId, executed.appliedAttentionSnapshot ?? attentionSnapshot);
|
|
1128
|
+
this.rememberRecentTurn(channelState, activeTurn);
|
|
1129
|
+
}
|
|
1130
|
+
if (executed.completed && executed.reply?.awaitingUser) {
|
|
1131
|
+
channelState.blocked = {
|
|
1132
|
+
awaitingUser: executed.reply.awaitingUser,
|
|
1133
|
+
suppressedAgentMessages: 0,
|
|
1134
|
+
};
|
|
1135
|
+
this.awaitingUserByChannel.set(channelId, executed.reply.awaitingUser);
|
|
1136
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1137
|
+
responseState: 'blocked',
|
|
1138
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1139
|
+
wakeState: 'waiting',
|
|
1140
|
+
blockedDetails: {
|
|
1141
|
+
question: executed.reply.awaitingUser.question,
|
|
1142
|
+
...(executed.reply.awaitingUser.reason
|
|
1143
|
+
? { reason: executed.reply.awaitingUser.reason }
|
|
1144
|
+
: {}),
|
|
1145
|
+
},
|
|
1146
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (!executed.completed && message.wakeBlockedState) {
|
|
1151
|
+
const existing = this.collaborationOutcomeByChannel.get(channelId);
|
|
1152
|
+
channelState.blocked = {
|
|
1153
|
+
awaitingUser: message.wakeBlockedState,
|
|
1154
|
+
suppressedAgentMessages: 0,
|
|
1155
|
+
};
|
|
1156
|
+
this.awaitingUserByChannel.set(channelId, message.wakeBlockedState);
|
|
1157
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1158
|
+
responseState: 'blocked',
|
|
1159
|
+
deliveryState: existing?.responseState === 'blocked' ? existing.deliveryState : 'not-delivered',
|
|
1160
|
+
wakeState: 'waiting',
|
|
1161
|
+
blockedDetails: {
|
|
1162
|
+
question: message.wakeBlockedState.question,
|
|
1163
|
+
...(message.wakeBlockedState.reason
|
|
1164
|
+
? { reason: message.wakeBlockedState.reason }
|
|
1165
|
+
: {}),
|
|
1166
|
+
},
|
|
1167
|
+
...(existing?.turnExecutionId ? { turnExecutionId: existing.turnExecutionId } : {}),
|
|
1168
|
+
});
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
channelState.blocked = undefined;
|
|
1172
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1173
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1174
|
+
responseState: executed.completed ? 'responded' : 'failed',
|
|
1175
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1176
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1177
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
async runProtocolManagedTurn(channelId, channelState, message) {
|
|
1181
|
+
const protocolState = channelState.protocol;
|
|
1182
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1183
|
+
if (!protocolState?.active || protocolState.expectedNextSpeakerId !== selfAgentId) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (!(await this.canContinueSharedProtocol(channelId, channelState, protocolState))) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
channelState.generation += 1;
|
|
1190
|
+
const currentTurnIndex = protocolState.currentTurnIndex + 1;
|
|
1191
|
+
const peerId = this.otherProtocolParticipant(protocolState, selfAgentId);
|
|
1192
|
+
const isFinalTurn = currentTurnIndex >= protocolState.maxTurnCount;
|
|
1193
|
+
const taskThreadContext = message.taskThreadContext
|
|
1194
|
+
?? await this.buildTurnTaskThreadContext(message.raw, channelId);
|
|
1195
|
+
const attentionSnapshot = message.attentionSnapshot
|
|
1196
|
+
?? this.getAttentionSnapshotForTurn(channelId, taskThreadContext);
|
|
1197
|
+
const activeTurn = {
|
|
1198
|
+
generation: channelState.generation,
|
|
1199
|
+
turnExecutionId: randomUUID(),
|
|
1200
|
+
superseded: false,
|
|
1201
|
+
collaborationMessagesSent: 0,
|
|
1202
|
+
targetCooldowns: new Map(),
|
|
1203
|
+
delivery: {
|
|
1204
|
+
replyToId: protocolState.lastProtocolMessageId ?? protocolState.anchorMessageId,
|
|
1205
|
+
},
|
|
1206
|
+
protocol: {
|
|
1207
|
+
anchorMessageId: protocolState.anchorMessageId,
|
|
1208
|
+
anchorSummary: summarizeAnchorContent(protocolState.anchorContent),
|
|
1209
|
+
issuerId: protocolState.issuerId,
|
|
1210
|
+
role: protocolState.currentTurnIndex === 0 ? 'issuer' : isFinalTurn ? 'final' : 'intermediate',
|
|
1211
|
+
targetPeerId: peerId,
|
|
1212
|
+
turnIndex: currentTurnIndex,
|
|
1213
|
+
maxTurnCount: protocolState.maxTurnCount,
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
this.clearCollaborationDraft(channelId);
|
|
1217
|
+
channelState.recentTurn = undefined;
|
|
1218
|
+
channelState.activeTurn = activeTurn;
|
|
1219
|
+
const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
|
|
1220
|
+
activeTurn,
|
|
1221
|
+
incomingAuthorKind: message.authorKind,
|
|
1222
|
+
attentionSnapshot,
|
|
1223
|
+
taskThreadContext,
|
|
1224
|
+
});
|
|
1225
|
+
channelState.activeTurn = undefined;
|
|
1226
|
+
if (executed.stale || !protocolState.active) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (executed.completed) {
|
|
1230
|
+
await this.commitAttentionSnapshot(channelId, executed.appliedAttentionSnapshot ?? attentionSnapshot);
|
|
1231
|
+
this.rememberRecentTurn(channelState, activeTurn);
|
|
1232
|
+
}
|
|
1233
|
+
const deliveredMessageId = executed.deliveredMessageId ?? null;
|
|
1234
|
+
const visibleMessageId = deliveredMessageId ?? protocolState.lastProtocolMessageId;
|
|
1235
|
+
if (executed.reply?.controlMalformed) {
|
|
1236
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1237
|
+
responseState: executed.completed ? 'responded' : 'failed',
|
|
1238
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1239
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1240
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1241
|
+
});
|
|
1242
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (!executed.reply?.control) {
|
|
1246
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1247
|
+
responseState: executed.completed ? 'responded' : 'failed',
|
|
1248
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1249
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1250
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1251
|
+
});
|
|
1252
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (executed.completed && executed.reply.control.kind === 'awaiting-user') {
|
|
1256
|
+
channelState.blocked = {
|
|
1257
|
+
awaitingUser: executed.reply.awaitingUser ?? {
|
|
1258
|
+
question: executed.reply.control.question,
|
|
1259
|
+
...(executed.reply.control.reason ? { reason: executed.reply.control.reason } : {}),
|
|
1260
|
+
},
|
|
1261
|
+
suppressedAgentMessages: 0,
|
|
1262
|
+
};
|
|
1263
|
+
this.awaitingUserByChannel.set(channelId, channelState.blocked.awaitingUser);
|
|
1264
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1265
|
+
responseState: 'blocked',
|
|
1266
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1267
|
+
wakeState: 'waiting',
|
|
1268
|
+
blockedDetails: {
|
|
1269
|
+
question: channelState.blocked.awaitingUser.question,
|
|
1270
|
+
...(channelState.blocked.awaitingUser.reason
|
|
1271
|
+
? { reason: channelState.blocked.awaitingUser.reason }
|
|
1272
|
+
: {}),
|
|
1273
|
+
},
|
|
1274
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1275
|
+
});
|
|
1276
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (executed.reply.control.kind === 'start-protocol') {
|
|
1280
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1281
|
+
responseState: executed.completed ? 'responded' : 'failed',
|
|
1282
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1283
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1284
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1285
|
+
});
|
|
1286
|
+
this.updateMissedCollaborationDiagnostic(channelId, undefined);
|
|
1287
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (executed.reply.control.kind === 'attention-only') {
|
|
1291
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1292
|
+
responseState: executed.completed ? 'responded' : 'failed',
|
|
1293
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1294
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1295
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1296
|
+
});
|
|
1297
|
+
this.updateMissedCollaborationDiagnostic(channelId, undefined);
|
|
1298
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (!deliveredMessageId) {
|
|
1302
|
+
this.finishProtocolState(channelId, channelState, protocolState);
|
|
1303
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1304
|
+
channelState.blocked = undefined;
|
|
1305
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1306
|
+
responseState: 'failed',
|
|
1307
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1308
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1309
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1310
|
+
});
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
protocolState.currentTurnIndex = currentTurnIndex;
|
|
1314
|
+
if (deliveredMessageId) {
|
|
1315
|
+
protocolState.lastProtocolMessageId = deliveredMessageId;
|
|
1316
|
+
}
|
|
1317
|
+
if (executed.reply.control.kind === 'conclude-locally') {
|
|
1318
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1319
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1320
|
+
channelState.blocked = undefined;
|
|
1321
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1322
|
+
responseState: 'responded',
|
|
1323
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1324
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1325
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1326
|
+
});
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (currentTurnIndex >= protocolState.maxTurnCount) {
|
|
1330
|
+
this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
|
|
1331
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1332
|
+
channelState.blocked = undefined;
|
|
1333
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1334
|
+
responseState: 'responded',
|
|
1335
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1336
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1337
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1338
|
+
});
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
protocolState.expectedNextSpeakerId = peerId;
|
|
1342
|
+
protocolState.fallbackDeadlineAt = nextProtocolFallbackDeadline();
|
|
1343
|
+
await this.persistProtocolState(channelId, protocolState);
|
|
1344
|
+
if (selfAgentId !== protocolState.issuerId) {
|
|
1345
|
+
this.armProtocolFallback(channelId, channelState, protocolState);
|
|
1346
|
+
}
|
|
1347
|
+
channelState.blocked = undefined;
|
|
1348
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
1349
|
+
this.recordCollaborationOutcome(channelId, {
|
|
1350
|
+
responseState: 'responded',
|
|
1351
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1352
|
+
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1353
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
buildIncomingContent(message) {
|
|
1357
|
+
if (!message.wakeBlockedState) {
|
|
1358
|
+
return message.content;
|
|
1359
|
+
}
|
|
1360
|
+
const lines = [
|
|
1361
|
+
'[Internal note: a previously blocked human reply has arrived.]',
|
|
1362
|
+
`Pending question: ${message.wakeBlockedState.question}`,
|
|
1363
|
+
...(message.wakeBlockedState.reason
|
|
1364
|
+
? [`Blocked reason: ${message.wakeBlockedState.reason}`]
|
|
1365
|
+
: []),
|
|
1366
|
+
`Human reply from ${message.authorId}: ${message.content}`,
|
|
1367
|
+
'',
|
|
1368
|
+
message.content,
|
|
1369
|
+
];
|
|
1370
|
+
return lines.join('\n');
|
|
1371
|
+
}
|
|
1372
|
+
otherProtocolParticipant(protocolState, currentAgentId) {
|
|
1373
|
+
return protocolState.participantIds[0] === currentAgentId
|
|
1374
|
+
? protocolState.participantIds[1]
|
|
1375
|
+
: protocolState.participantIds[0];
|
|
1376
|
+
}
|
|
1377
|
+
resolveProtocolStatePath(channelId, ownerAgentId) {
|
|
1378
|
+
return resolve(this.config.stateRootDir, PROTOCOL_STATE_DIRNAME, `${encodeStatePathSegment(channelId)}--${encodeStatePathSegment(ownerAgentId)}.json`);
|
|
1379
|
+
}
|
|
1380
|
+
async ensureProtocolStateLoaded(channelId, channelState) {
|
|
1381
|
+
if (channelState.protocolLoaded) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (channelState.protocol) {
|
|
1385
|
+
channelState.protocolLoaded = true;
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
channelState.protocolLoaded = true;
|
|
1389
|
+
try {
|
|
1390
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1391
|
+
const content = await readFile(this.resolveProtocolStatePath(channelId, selfAgentId), 'utf8');
|
|
1392
|
+
const parsed = JSON.parse(content);
|
|
1393
|
+
if ((parsed.ownerAgentId && parsed.ownerAgentId !== selfAgentId) ||
|
|
1394
|
+
!parsed.participantIds.includes(selfAgentId)) {
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const resolvedProtocolState = await this.reconcileOwnProtocolStateFromPeers(channelId, {
|
|
1398
|
+
...parsed,
|
|
1399
|
+
ownerAgentId: parsed.ownerAgentId ?? selfAgentId,
|
|
1400
|
+
}, selfAgentId);
|
|
1401
|
+
channelState.protocol = {
|
|
1402
|
+
...resolvedProtocolState,
|
|
1403
|
+
fallbackTimer: null,
|
|
1404
|
+
};
|
|
1405
|
+
if (resolvedProtocolState.currentTurnIndex !== parsed.currentTurnIndex ||
|
|
1406
|
+
resolvedProtocolState.expectedNextSpeakerId !== parsed.expectedNextSpeakerId ||
|
|
1407
|
+
resolvedProtocolState.active !== parsed.active ||
|
|
1408
|
+
resolvedProtocolState.finished !== parsed.finished ||
|
|
1409
|
+
resolvedProtocolState.lastProtocolMessageId !== parsed.lastProtocolMessageId) {
|
|
1410
|
+
await this.persistProtocolState(channelId, channelState.protocol);
|
|
1411
|
+
}
|
|
1412
|
+
if (resolvedProtocolState.active) {
|
|
1413
|
+
this.armProtocolFallback(channelId, channelState, channelState.protocol);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
catch (error) {
|
|
1417
|
+
const code = error.code;
|
|
1418
|
+
if (code !== 'ENOENT') {
|
|
1419
|
+
this.logger.error('failed to load protocol state', {
|
|
1420
|
+
channelId,
|
|
1421
|
+
error: summarizeError(error),
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
async reconcileOwnProtocolStateFromPeers(channelId, protocolState, selfAgentId) {
|
|
1427
|
+
const directoryPath = join(this.config.stateRootDir, PROTOCOL_STATE_DIRNAME);
|
|
1428
|
+
const channelPrefix = `${encodeStatePathSegment(channelId)}--`;
|
|
1429
|
+
const ownSuffix = `--${encodeStatePathSegment(selfAgentId)}.json`;
|
|
1430
|
+
let entries;
|
|
1431
|
+
try {
|
|
1432
|
+
entries = await readdir(directoryPath);
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
const code = error.code;
|
|
1436
|
+
if (code === 'ENOENT') {
|
|
1437
|
+
return protocolState;
|
|
1438
|
+
}
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
let resolved = protocolState;
|
|
1442
|
+
for (const entry of entries) {
|
|
1443
|
+
if (!entry.startsWith(channelPrefix) || entry.endsWith(ownSuffix)) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
const candidate = JSON.parse(await readFile(join(directoryPath, entry), 'utf8'));
|
|
1448
|
+
if (!candidate.participantIds.includes(selfAgentId)) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
if (!shouldAdoptPeerProtocolState(resolved, candidate, selfAgentId)) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
resolved = {
|
|
1455
|
+
...candidate,
|
|
1456
|
+
ownerAgentId: selfAgentId,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
const code = error.code;
|
|
1461
|
+
if (code === 'ENOENT') {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return resolved;
|
|
1468
|
+
}
|
|
1469
|
+
async loadPersistedProtocolStates() {
|
|
1470
|
+
if (!this.collaborationEnabled) {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1474
|
+
const ownSuffix = `--${encodeStatePathSegment(selfAgentId)}.json`;
|
|
1475
|
+
const directoryPath = join(this.config.stateRootDir, PROTOCOL_STATE_DIRNAME);
|
|
1476
|
+
let entries;
|
|
1477
|
+
try {
|
|
1478
|
+
entries = await readdir(directoryPath);
|
|
1479
|
+
}
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
const code = error.code;
|
|
1482
|
+
if (code !== 'ENOENT') {
|
|
1483
|
+
this.logger.error('failed to enumerate protocol state directory', {
|
|
1484
|
+
directoryPath,
|
|
1485
|
+
error: summarizeError(error),
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
for (const entry of entries) {
|
|
1491
|
+
if (!entry.endsWith(ownSuffix)) {
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
const encodedChannelId = entry.slice(0, -ownSuffix.length);
|
|
1495
|
+
const channelId = decodeStatePathSegment(encodedChannelId);
|
|
1496
|
+
if (channelId == null) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const channelState = this.ensureCollaborationChannelState(channelId);
|
|
1500
|
+
await this.ensureProtocolStateLoaded(channelId, channelState);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async resumeOwnedProtocolTurns() {
|
|
1504
|
+
if (!this.collaborationEnabled) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1508
|
+
for (const [channelId, channelState] of this.collaborationChannels.entries()) {
|
|
1509
|
+
const protocolState = channelState.protocol;
|
|
1510
|
+
if (!protocolState?.active || protocolState.expectedNextSpeakerId !== selfAgentId) {
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (channelState.activeTurn ||
|
|
1514
|
+
channelState.queue.some((message) => message.kind === 'protocol-turn')) {
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
const resumed = await this.buildResumedProtocolMessage(channelId, protocolState);
|
|
1518
|
+
if (!resumed) {
|
|
1519
|
+
this.cancelProtocolState(channelId, channelState);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
this.enqueueCollaborationMessage(channelState, resumed);
|
|
1523
|
+
await this.ensureCollaborationProcessor(channelId, channelState);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
async buildResumedProtocolMessage(channelId, protocolState) {
|
|
1527
|
+
if (protocolState.currentTurnIndex === 0) {
|
|
1528
|
+
return {
|
|
1529
|
+
kind: 'protocol-turn',
|
|
1530
|
+
raw: {
|
|
1531
|
+
channel_id: channelId,
|
|
1532
|
+
message_id: protocolState.anchorMessageId,
|
|
1533
|
+
user_id: protocolState.anchorAuthorId,
|
|
1534
|
+
content: protocolState.anchorContent,
|
|
1535
|
+
body: protocolState.anchorContent,
|
|
1536
|
+
type: 'message',
|
|
1537
|
+
},
|
|
1538
|
+
authorId: protocolState.anchorAuthorId,
|
|
1539
|
+
authorKind: 'human',
|
|
1540
|
+
content: protocolState.anchorContent,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
if (!protocolState.lastProtocolMessageId) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
let before;
|
|
1547
|
+
for (;;) {
|
|
1548
|
+
const history = await this.borgee.readChannelHistory({
|
|
1549
|
+
channelId,
|
|
1550
|
+
before,
|
|
1551
|
+
limit: 200,
|
|
1552
|
+
});
|
|
1553
|
+
const priorMessage = history.find((entry) => entry.id === protocolState.lastProtocolMessageId);
|
|
1554
|
+
if (priorMessage) {
|
|
1555
|
+
return {
|
|
1556
|
+
kind: 'protocol-turn',
|
|
1557
|
+
raw: {
|
|
1558
|
+
channel_id: channelId,
|
|
1559
|
+
message_id: priorMessage.id,
|
|
1560
|
+
user_id: priorMessage.authorId,
|
|
1561
|
+
content: priorMessage.body,
|
|
1562
|
+
body: priorMessage.body,
|
|
1563
|
+
type: 'message',
|
|
1564
|
+
created_at: priorMessage.createdAt,
|
|
1565
|
+
},
|
|
1566
|
+
authorId: priorMessage.authorId,
|
|
1567
|
+
authorKind: 'agent',
|
|
1568
|
+
content: priorMessage.body,
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
if (history.length < 200) {
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
const oldestCreatedAt = history.reduce((oldest, entry) => {
|
|
1575
|
+
return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
|
|
1576
|
+
}, null);
|
|
1577
|
+
if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
before = oldestCreatedAt;
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
async persistProtocolState(channelId, protocolState) {
|
|
1585
|
+
const ownerAgentId = protocolState.ownerAgentId ?? (await this.ensureSelfAgentId());
|
|
1586
|
+
protocolState.ownerAgentId = ownerAgentId;
|
|
1587
|
+
await writePrivateJsonFile(this.resolveProtocolStatePath(channelId, ownerAgentId), {
|
|
1588
|
+
ownerAgentId,
|
|
1589
|
+
anchorAuthorId: protocolState.anchorAuthorId,
|
|
1590
|
+
anchorContent: protocolState.anchorContent,
|
|
1591
|
+
anchorMessageId: protocolState.anchorMessageId,
|
|
1592
|
+
participantIds: protocolState.participantIds,
|
|
1593
|
+
issuerId: protocolState.issuerId,
|
|
1594
|
+
expectedNextSpeakerId: protocolState.expectedNextSpeakerId,
|
|
1595
|
+
currentTurnIndex: protocolState.currentTurnIndex,
|
|
1596
|
+
maxTurnCount: protocolState.maxTurnCount,
|
|
1597
|
+
lastProtocolMessageId: protocolState.lastProtocolMessageId,
|
|
1598
|
+
active: protocolState.active,
|
|
1599
|
+
finished: protocolState.finished,
|
|
1600
|
+
fallbackDeadlineAt: protocolState.fallbackDeadlineAt,
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
async persistSharedProtocolStatus(channelId, protocolState) {
|
|
1604
|
+
await writePrivateJsonFile(this.resolveSharedProtocolStatusPath(channelId, protocolState.anchorMessageId), {
|
|
1605
|
+
channelId,
|
|
1606
|
+
anchorMessageId: protocolState.anchorMessageId,
|
|
1607
|
+
active: protocolState.active,
|
|
1608
|
+
finished: protocolState.finished,
|
|
1609
|
+
updatedAt: Date.now(),
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
async readSharedProtocolStatus(channelId, anchorMessageId) {
|
|
1613
|
+
try {
|
|
1614
|
+
const content = await readFile(this.resolveSharedProtocolStatusPath(channelId, anchorMessageId), 'utf8');
|
|
1615
|
+
const parsed = JSON.parse(content);
|
|
1616
|
+
if (typeof parsed.active !== 'boolean' || typeof parsed.finished !== 'boolean') {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
return { active: parsed.active, finished: parsed.finished };
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
const code = error.code;
|
|
1623
|
+
if (code === 'ENOENT') {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
throw error;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
clearProtocolFallback(channelState) {
|
|
1630
|
+
const timer = channelState?.protocol?.fallbackTimer;
|
|
1631
|
+
if (timer) {
|
|
1632
|
+
clearTimeout(timer);
|
|
1633
|
+
if (channelState?.protocol) {
|
|
1634
|
+
channelState.protocol.fallbackTimer = null;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
armProtocolFallback(channelId, channelState, protocolState) {
|
|
1639
|
+
this.clearProtocolFallback(channelState);
|
|
1640
|
+
const delay = Math.max(0, protocolState.fallbackDeadlineAt - Date.now());
|
|
1641
|
+
protocolState.fallbackTimer = setTimeout(() => {
|
|
1642
|
+
void this.runProtocolFallback(channelId).catch((error) => {
|
|
1643
|
+
this.logger.error('protocol fallback failed', {
|
|
1644
|
+
channelId,
|
|
1645
|
+
error: summarizeError(error),
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
}, delay);
|
|
1649
|
+
}
|
|
1650
|
+
async runProtocolFallback(channelId) {
|
|
1651
|
+
const channelState = this.collaborationChannels.get(channelId);
|
|
1652
|
+
const protocolState = channelState?.protocol;
|
|
1653
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
1654
|
+
if (!channelState ||
|
|
1655
|
+
!protocolState?.active ||
|
|
1656
|
+
selfAgentId === protocolState.issuerId ||
|
|
1657
|
+
protocolState.expectedNextSpeakerId !== protocolState.issuerId ||
|
|
1658
|
+
protocolState.fallbackDeadlineAt > Date.now()) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
await this.handleProtocolFallbackFailure(channelId, channelState, protocolState, selfAgentId);
|
|
1662
|
+
}
|
|
1663
|
+
async handleProtocolFallbackFailure(channelId, channelState, protocolState, selfAgentId) {
|
|
1664
|
+
if (selfAgentId === protocolState.issuerId) {
|
|
1665
|
+
protocolState.fallbackDeadlineAt = nextProtocolFallbackDeadline();
|
|
1666
|
+
await this.persistProtocolState(channelId, protocolState);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const fallbackMessage = {
|
|
1670
|
+
channel_id: channelId,
|
|
1671
|
+
message_id: protocolState.anchorMessageId,
|
|
1672
|
+
user_id: protocolState.anchorAuthorId,
|
|
1673
|
+
content: protocolState.anchorContent,
|
|
1674
|
+
body: protocolState.anchorContent,
|
|
1675
|
+
type: 'message',
|
|
1676
|
+
};
|
|
1677
|
+
this.cancelProtocolState(channelId, channelState);
|
|
1678
|
+
const taskThreadContext = await this.buildTurnTaskThreadContext(fallbackMessage, channelId);
|
|
1679
|
+
const attentionSnapshot = this.getAttentionSnapshotForTurn(channelId, taskThreadContext);
|
|
1680
|
+
await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, protocolState.anchorContent, taskThreadContext, attentionSnapshot);
|
|
1681
|
+
}
|
|
1682
|
+
finishProtocolState(channelId, channelState, protocolState, lastMessageId) {
|
|
1683
|
+
protocolState.active = false;
|
|
1684
|
+
protocolState.finished = true;
|
|
1685
|
+
if (lastMessageId) {
|
|
1686
|
+
protocolState.lastProtocolMessageId = lastMessageId;
|
|
1687
|
+
}
|
|
1688
|
+
this.clearProtocolFallback(channelState);
|
|
1689
|
+
void this.persistProtocolState(channelId, protocolState);
|
|
1690
|
+
void this.persistSharedProtocolStatus(channelId, protocolState);
|
|
1691
|
+
}
|
|
1692
|
+
cancelProtocolState(channelId, channelState) {
|
|
1693
|
+
if (channelState.activeTurn) {
|
|
1694
|
+
this.supersedeActiveTurn(channelId, channelState.activeTurn);
|
|
1695
|
+
}
|
|
1696
|
+
if (!channelState.protocol) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
channelState.protocol.active = false;
|
|
1700
|
+
channelState.protocol.finished = false;
|
|
1701
|
+
this.clearProtocolFallback(channelState);
|
|
1702
|
+
void this.persistProtocolState(channelId, channelState.protocol);
|
|
1703
|
+
void this.persistSharedProtocolStatus(channelId, channelState.protocol);
|
|
1704
|
+
}
|
|
1705
|
+
async confirmProtocolMessage(channelId, messageId, authorId, expectedReplyToId, createdAt) {
|
|
1706
|
+
const history = await this.borgee.readChannelHistory({
|
|
1707
|
+
channelId,
|
|
1708
|
+
before: createdAt != null ? createdAt + 1 : undefined,
|
|
1709
|
+
limit: 20,
|
|
1710
|
+
});
|
|
1711
|
+
const matched = history.find((entry) => entry.id === messageId);
|
|
1712
|
+
if (!matched) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
if (matched.authorId !== authorId || matched.replyToId !== expectedReplyToId) {
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
return matched;
|
|
1719
|
+
}
|
|
1720
|
+
resolveAttentionStatePath(channelId) {
|
|
1721
|
+
return resolveAttentionStatePath(this.config.stateRootDir, channelId);
|
|
1722
|
+
}
|
|
1723
|
+
buildDefaultAttentionSnapshot(options) {
|
|
1724
|
+
return buildNormalizedAttentionSnapshot({
|
|
1725
|
+
observedAt: options?.observedAt,
|
|
1726
|
+
turnExecutionId: options?.turnExecutionId,
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
async buildTurnTaskThreadContext(msg, channelId) {
|
|
1730
|
+
if (msg.message_type === 'task_assignment') {
|
|
1731
|
+
const currentTaskId = extractTaskIdFromTaskAssignmentContent(String(msg.content ?? msg.body ?? ''));
|
|
1732
|
+
return currentTaskId ? { state: 'active', currentTaskId } : { state: 'active' };
|
|
1733
|
+
}
|
|
1734
|
+
if (!this.contextInjectionEnabled) {
|
|
1735
|
+
return { state: 'inactive' };
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
const content = await readFile(resolveChannelContextPayloadPath(this.config.stateRootDir, channelId), 'utf8');
|
|
1739
|
+
const payload = JSON.parse(content);
|
|
1740
|
+
if (payload.taskAssignmentContext?.active !== true) {
|
|
1741
|
+
return { state: 'inactive' };
|
|
1742
|
+
}
|
|
1743
|
+
const currentTaskId = typeof payload.taskAssignmentContext.currentTaskId === 'string'
|
|
1744
|
+
? payload.taskAssignmentContext.currentTaskId.trim()
|
|
1745
|
+
: '';
|
|
1746
|
+
return currentTaskId ? { state: 'active', currentTaskId } : { state: 'active' };
|
|
1747
|
+
}
|
|
1748
|
+
catch {
|
|
1749
|
+
return { state: 'unavailable' };
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async ensureAttentionSnapshotLoaded(channelId, channelState) {
|
|
1753
|
+
if (!this.attentionFollowSemanticsEnabled) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (channelState?.attentionLoaded) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (channelState) {
|
|
1760
|
+
channelState.attentionLoaded = true;
|
|
1761
|
+
}
|
|
1762
|
+
try {
|
|
1763
|
+
const content = await readFile(this.resolveAttentionStatePath(channelId), 'utf8');
|
|
1764
|
+
this.attentionSnapshotByChannel.set(channelId, parsePersistedAttentionSnapshot(JSON.parse(content)));
|
|
1765
|
+
}
|
|
1766
|
+
catch (error) {
|
|
1767
|
+
const code = error.code;
|
|
1768
|
+
if (code !== 'ENOENT') {
|
|
1769
|
+
this.logger.error('failed to load attention state', {
|
|
1770
|
+
channelId,
|
|
1771
|
+
error: summarizeError(error),
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
this.attentionSnapshotByChannel.set(channelId, this.buildDefaultAttentionSnapshot());
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
revalidateAttentionSnapshotForTurn(channelId, taskThreadContext, options) {
|
|
1778
|
+
const current = this.attentionSnapshotByChannel.get(channelId)
|
|
1779
|
+
?? this.buildDefaultAttentionSnapshot();
|
|
1780
|
+
if (current.claimContext?.scope !== 'task-thread') {
|
|
1781
|
+
return current;
|
|
1782
|
+
}
|
|
1783
|
+
if (!this.contextInjectionEnabled) {
|
|
1784
|
+
const next = this.buildDefaultAttentionSnapshot({
|
|
1785
|
+
observedAt: options?.observedAt ?? Date.now(),
|
|
1786
|
+
turnExecutionId: options?.turnExecutionId,
|
|
1787
|
+
});
|
|
1788
|
+
this.attentionSnapshotByChannel.set(channelId, next);
|
|
1789
|
+
return next;
|
|
1790
|
+
}
|
|
1791
|
+
if (taskThreadContext.state === 'active' &&
|
|
1792
|
+
taskThreadContext.currentTaskId &&
|
|
1793
|
+
taskThreadContext.currentTaskId === current.claimContext.taskId) {
|
|
1794
|
+
const next = buildNormalizedAttentionSnapshot({
|
|
1795
|
+
...current,
|
|
1796
|
+
deliveryMode: 'follow',
|
|
1797
|
+
claimState: 'claimed',
|
|
1798
|
+
claimActivation: 'active',
|
|
1799
|
+
observedAt: options?.observedAt ?? Date.now(),
|
|
1800
|
+
turnExecutionId: options?.turnExecutionId,
|
|
1801
|
+
claimContext: current.claimContext,
|
|
1802
|
+
});
|
|
1803
|
+
this.attentionSnapshotByChannel.set(channelId, next);
|
|
1804
|
+
return next;
|
|
1805
|
+
}
|
|
1806
|
+
if (taskThreadContext.state === 'unavailable') {
|
|
1807
|
+
const next = buildNormalizedAttentionSnapshot({
|
|
1808
|
+
...current,
|
|
1809
|
+
deliveryMode: 'default',
|
|
1810
|
+
claimState: 'claimed',
|
|
1811
|
+
claimActivation: 'dormant',
|
|
1812
|
+
observedAt: options?.observedAt ?? Date.now(),
|
|
1813
|
+
turnExecutionId: options?.turnExecutionId,
|
|
1814
|
+
claimContext: current.claimContext,
|
|
1815
|
+
});
|
|
1816
|
+
this.attentionSnapshotByChannel.set(channelId, next);
|
|
1817
|
+
return next;
|
|
1818
|
+
}
|
|
1819
|
+
const next = this.buildDefaultAttentionSnapshot({
|
|
1820
|
+
observedAt: options?.observedAt ?? Date.now(),
|
|
1821
|
+
turnExecutionId: options?.turnExecutionId,
|
|
1822
|
+
});
|
|
1823
|
+
this.attentionSnapshotByChannel.set(channelId, next);
|
|
1824
|
+
return next;
|
|
1825
|
+
}
|
|
1826
|
+
getAttentionSnapshotForTurn(channelId, taskThreadContext, options) {
|
|
1827
|
+
if (!this.attentionFollowSemanticsEnabled) {
|
|
1828
|
+
return undefined;
|
|
1829
|
+
}
|
|
1830
|
+
return this.revalidateAttentionSnapshotForTurn(channelId, taskThreadContext, options);
|
|
1831
|
+
}
|
|
1832
|
+
isHumanWakeFiltered(msg, authorKind, attentionSnapshot) {
|
|
1833
|
+
if (!this.attentionFollowSemanticsEnabled || authorKind === 'agent') {
|
|
1834
|
+
return false;
|
|
1835
|
+
}
|
|
1836
|
+
if (msg.message_type === 'task_assignment') {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
if (this.awaitingUserByChannel.has(msg.channel_id)) {
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
if (msg.type?.trim() === 'mention') {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
const eventKind = msg.type?.trim() ?? '';
|
|
1846
|
+
if (!ORDINARY_HUMAN_EVENT_KINDS.has(eventKind)) {
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
if (attentionSnapshot?.wakeMode === 'follow-visible-human') {
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
this.noteWakeSuppressedAttentionPolicy(msg, authorKind);
|
|
1853
|
+
return true;
|
|
1854
|
+
}
|
|
1855
|
+
applyAttentionUpdate(channelId, baseSnapshot, attentionUpdate, taskThreadContext, options) {
|
|
1856
|
+
if (!this.attentionFollowSemanticsEnabled) {
|
|
1857
|
+
return undefined;
|
|
1858
|
+
}
|
|
1859
|
+
if (!attentionUpdate) {
|
|
1860
|
+
return baseSnapshot ?? this.attentionSnapshotByChannel.get(channelId);
|
|
1861
|
+
}
|
|
1862
|
+
const observedAt = options?.observedAt ?? Date.now();
|
|
1863
|
+
const turnExecutionId = options?.turnExecutionId;
|
|
1864
|
+
const current = baseSnapshot
|
|
1865
|
+
?? this.attentionSnapshotByChannel.get(channelId)
|
|
1866
|
+
?? this.buildDefaultAttentionSnapshot();
|
|
1867
|
+
const clearClaim = (deliveryMode) => buildNormalizedAttentionSnapshot({
|
|
1868
|
+
deliveryMode,
|
|
1869
|
+
claimState: 'unclaimed',
|
|
1870
|
+
claimActivation: 'inactive',
|
|
1871
|
+
observedAt,
|
|
1872
|
+
turnExecutionId,
|
|
1873
|
+
});
|
|
1874
|
+
switch (attentionUpdate) {
|
|
1875
|
+
case 'follow-channel':
|
|
1876
|
+
return clearClaim('follow');
|
|
1877
|
+
case 'unfollow-channel':
|
|
1878
|
+
return clearClaim('default');
|
|
1879
|
+
case 'mute-channel':
|
|
1880
|
+
return clearClaim('muted');
|
|
1881
|
+
case 'claim-channel':
|
|
1882
|
+
return buildNormalizedAttentionSnapshot({
|
|
1883
|
+
deliveryMode: 'follow',
|
|
1884
|
+
claimState: 'claimed',
|
|
1885
|
+
claimActivation: 'active',
|
|
1886
|
+
observedAt,
|
|
1887
|
+
turnExecutionId,
|
|
1888
|
+
});
|
|
1889
|
+
case 'claim-task-thread':
|
|
1890
|
+
if (!this.contextInjectionEnabled
|
|
1891
|
+
|| taskThreadContext.state !== 'active'
|
|
1892
|
+
|| !taskThreadContext.currentTaskId) {
|
|
1893
|
+
return current;
|
|
1894
|
+
}
|
|
1895
|
+
return buildNormalizedAttentionSnapshot({
|
|
1896
|
+
deliveryMode: 'follow',
|
|
1897
|
+
claimState: 'claimed',
|
|
1898
|
+
claimActivation: 'active',
|
|
1899
|
+
observedAt,
|
|
1900
|
+
turnExecutionId,
|
|
1901
|
+
claimContext: {
|
|
1902
|
+
scope: 'task-thread',
|
|
1903
|
+
taskId: taskThreadContext.currentTaskId,
|
|
1904
|
+
},
|
|
1905
|
+
});
|
|
1906
|
+
case 'unclaim-channel':
|
|
1907
|
+
return buildNormalizedAttentionSnapshot({
|
|
1908
|
+
deliveryMode: current.deliveryMode,
|
|
1909
|
+
claimState: 'unclaimed',
|
|
1910
|
+
claimActivation: 'inactive',
|
|
1911
|
+
observedAt,
|
|
1912
|
+
turnExecutionId,
|
|
1913
|
+
});
|
|
1914
|
+
case 'unclaim-task-thread':
|
|
1915
|
+
if (!this.contextInjectionEnabled
|
|
1916
|
+
|| current.claimContext?.scope !== 'task-thread'
|
|
1917
|
+
|| taskThreadContext.state !== 'active'
|
|
1918
|
+
|| !taskThreadContext.currentTaskId
|
|
1919
|
+
|| taskThreadContext.currentTaskId !== current.claimContext.taskId) {
|
|
1920
|
+
return current;
|
|
1921
|
+
}
|
|
1922
|
+
return buildNormalizedAttentionSnapshot({
|
|
1923
|
+
deliveryMode: current.deliveryMode,
|
|
1924
|
+
claimState: 'unclaimed',
|
|
1925
|
+
claimActivation: 'inactive',
|
|
1926
|
+
observedAt,
|
|
1927
|
+
turnExecutionId,
|
|
1928
|
+
});
|
|
1929
|
+
default:
|
|
1930
|
+
return current;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
async commitAttentionSnapshot(channelId, snapshot) {
|
|
1934
|
+
if (!this.attentionFollowSemanticsEnabled || !snapshot) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
this.attentionSnapshotByChannel.set(channelId, snapshot);
|
|
1938
|
+
await writePrivateJsonFile(this.resolveAttentionStatePath(channelId), snapshot);
|
|
1939
|
+
}
|
|
1940
|
+
resolveTaskThreadCollaborationContractForTurn(msg, taskThreadContext) {
|
|
1941
|
+
if (!this.taskThreadCollaborationContractEnabled) {
|
|
1942
|
+
return undefined;
|
|
1943
|
+
}
|
|
1944
|
+
return projectTaskThreadCollaborationContract({
|
|
1945
|
+
incomingMessageType: msg.message_type,
|
|
1946
|
+
taskThreadContextActive: taskThreadContext.state === 'active',
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
resolveCollaborationCapabilityDeclarationForTurn() {
|
|
1950
|
+
if (!this.collaborationCapabilitiesDiagnosticsEnabled) {
|
|
1951
|
+
return undefined;
|
|
1952
|
+
}
|
|
1953
|
+
return {
|
|
1954
|
+
...(this.collaborationOutcomeModelEnabled ? { collaborationOutcome: true } : {}),
|
|
1955
|
+
...(this.attentionFollowSemanticsEnabled ? { attentionSnapshot: true } : {}),
|
|
1956
|
+
...(this.taskThreadCollaborationContractEnabled
|
|
1957
|
+
? { taskThreadCollaborationContract: true }
|
|
1958
|
+
: {}),
|
|
1959
|
+
missedCollaborationDiagnostic: true,
|
|
1960
|
+
recoveryExplanation: 'runtime-local-only',
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
resolveMissedCollaborationDiagnosticForTurn(channelId) {
|
|
1964
|
+
if (!this.collaborationCapabilitiesDiagnosticsEnabled) {
|
|
1965
|
+
return undefined;
|
|
1966
|
+
}
|
|
1967
|
+
return this.missedCollaborationDiagnosticByChannel.get(channelId);
|
|
1968
|
+
}
|
|
1969
|
+
updateMissedCollaborationDiagnostic(channelId, diagnostic) {
|
|
1970
|
+
if (!this.collaborationCapabilitiesDiagnosticsEnabled || !diagnostic) {
|
|
1971
|
+
this.missedCollaborationDiagnosticByChannel.delete(channelId);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
this.missedCollaborationDiagnosticByChannel.set(channelId, diagnostic);
|
|
1975
|
+
}
|
|
1976
|
+
noteWakeSuppressedAttentionPolicy(msg, authorKind) {
|
|
1977
|
+
if (authorKind === 'agent' || msg.message_type === 'task_assignment') {
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const eventKind = msg.type?.trim() ?? '';
|
|
1981
|
+
if (msg.type?.trim() === 'mention' || !ORDINARY_HUMAN_EVENT_KINDS.has(eventKind)) {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
this.updateMissedCollaborationDiagnostic(msg.channel_id, {
|
|
1985
|
+
stage: 'wake',
|
|
1986
|
+
reason: 'wake-suppressed-attention-policy',
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot) {
|
|
1990
|
+
const executed = await this.executeTurn(msg, authorId, content, {
|
|
1991
|
+
attentionSnapshot,
|
|
1992
|
+
taskThreadContext,
|
|
1993
|
+
});
|
|
1994
|
+
if (executed.completed && executed.reply) {
|
|
1995
|
+
this.updateAwaitingUserState(msg.channel_id, executed.reply.awaitingUser);
|
|
1996
|
+
}
|
|
1997
|
+
if (executed.completed) {
|
|
1998
|
+
await this.commitAttentionSnapshot(msg.channel_id, executed.appliedAttentionSnapshot ?? attentionSnapshot);
|
|
1999
|
+
}
|
|
2000
|
+
if (!executed.completed) {
|
|
2001
|
+
const awaitingUser = this.awaitingUserByChannel.get(msg.channel_id);
|
|
2002
|
+
if (awaitingUser) {
|
|
2003
|
+
const existing = this.collaborationOutcomeByChannel.get(msg.channel_id);
|
|
2004
|
+
this.recordCollaborationOutcome(msg.channel_id, {
|
|
2005
|
+
responseState: 'blocked',
|
|
2006
|
+
deliveryState: existing?.responseState === 'blocked' ? existing.deliveryState : 'not-delivered',
|
|
2007
|
+
wakeState: 'waiting',
|
|
2008
|
+
blockedDetails: {
|
|
2009
|
+
question: awaitingUser.question,
|
|
2010
|
+
...(awaitingUser.reason ? { reason: awaitingUser.reason } : {}),
|
|
2011
|
+
},
|
|
2012
|
+
...(existing?.turnExecutionId ? { turnExecutionId: existing.turnExecutionId } : {}),
|
|
2013
|
+
});
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
this.recordCollaborationOutcome(msg.channel_id, {
|
|
2018
|
+
responseState: executed.completed && executed.reply?.awaitingUser
|
|
2019
|
+
? 'blocked'
|
|
2020
|
+
: executed.completed
|
|
2021
|
+
? 'responded'
|
|
2022
|
+
: 'failed',
|
|
2023
|
+
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
2024
|
+
...(executed.completed && executed.reply?.awaitingUser
|
|
2025
|
+
? {
|
|
2026
|
+
wakeState: 'waiting',
|
|
2027
|
+
blockedDetails: {
|
|
2028
|
+
question: executed.reply.awaitingUser.question,
|
|
2029
|
+
...(executed.reply.awaitingUser.reason
|
|
2030
|
+
? { reason: executed.reply.awaitingUser.reason }
|
|
2031
|
+
: {}),
|
|
2032
|
+
},
|
|
2033
|
+
}
|
|
2034
|
+
: {}),
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
async executeTurn(msg, authorId, content, options) {
|
|
2038
|
+
const channelId = msg.channel_id;
|
|
2039
|
+
const activeTurn = options?.activeTurn;
|
|
2040
|
+
const attentionSnapshot = options?.attentionSnapshot;
|
|
2041
|
+
const taskThreadContext = options?.taskThreadContext ?? { state: 'inactive' };
|
|
2042
|
+
const silentTurn = options?.silent === true;
|
|
2043
|
+
const deferDelivery = options?.deferDelivery === true;
|
|
2044
|
+
const collaborationTurnMode = activeTurn?.protocol
|
|
2045
|
+
? 'protocol-managed'
|
|
2046
|
+
: options?.kickoff
|
|
2047
|
+
? 'silent-kickoff'
|
|
2048
|
+
: undefined;
|
|
2049
|
+
const turnExecutionId = activeTurn?.turnExecutionId ?? null;
|
|
2050
|
+
const stopTyping = silentTurn ? () => { } : this.borgee.startTyping(channelId);
|
|
2051
|
+
const collaborationGrounding = await this.buildCollaborationGrounding(authorId, options?.incomingAuthorKind, activeTurn, options?.kickoff);
|
|
2052
|
+
const usePublicDraftProgress = !silentTurn &&
|
|
2053
|
+
!deferDelivery &&
|
|
2054
|
+
!this.collaborationEnabled &&
|
|
2055
|
+
options?.incomingAuthorKind !== 'agent' &&
|
|
2056
|
+
providerUsesDraftProgress(this.config.agent.provider) &&
|
|
2057
|
+
activeTurn?.protocol === undefined;
|
|
2058
|
+
const usePrivateCollaborationDraftProgress = !silentTurn &&
|
|
2059
|
+
!deferDelivery &&
|
|
2060
|
+
this.collaborationEnabled &&
|
|
2061
|
+
turnExecutionId != null &&
|
|
2062
|
+
providerUsesDraftProgress(this.config.agent.provider) &&
|
|
2063
|
+
activeTurn?.protocol === undefined;
|
|
2064
|
+
const turnContext = {
|
|
2065
|
+
eventType: msg.type,
|
|
2066
|
+
channelId,
|
|
2067
|
+
messageId: msg.message_id,
|
|
2068
|
+
authorId,
|
|
2069
|
+
contentLength: content.length,
|
|
2070
|
+
provider: this.config.agent.provider,
|
|
2071
|
+
agentName: this.config.agent.agentName,
|
|
2072
|
+
generation: activeTurn?.generation,
|
|
2073
|
+
};
|
|
2074
|
+
const turnStartedAt = Date.now();
|
|
2075
|
+
this.logger.debug('starting turn', turnContext);
|
|
2076
|
+
let reply;
|
|
2077
|
+
let publicReplyText;
|
|
2078
|
+
let visibleReplyBody;
|
|
2079
|
+
const taskThreadCollaborationContract = this.resolveTaskThreadCollaborationContractForTurn(msg, taskThreadContext);
|
|
2080
|
+
const collaborationOutcome = this.collaborationOutcomeModelEnabled
|
|
2081
|
+
? this.collaborationOutcomeByChannel.get(channelId)
|
|
2082
|
+
: undefined;
|
|
2083
|
+
const missedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(channelId);
|
|
2084
|
+
const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn();
|
|
2085
|
+
try {
|
|
2086
|
+
reply = await this.provider.generateReply({
|
|
2087
|
+
agentName: this.config.agent.agentName,
|
|
2088
|
+
provider: this.config.agent.provider,
|
|
2089
|
+
channelId,
|
|
2090
|
+
incomingAuthorId: authorId,
|
|
2091
|
+
incomingContent: content,
|
|
2092
|
+
incomingEventKind: msg.type,
|
|
2093
|
+
incomingMessageType: msg.message_type,
|
|
2094
|
+
collaboration: turnExecutionId || options?.kickoff
|
|
2095
|
+
? {
|
|
2096
|
+
enabled: this.collaborationEnabled,
|
|
2097
|
+
...(turnExecutionId ? { turnExecutionId } : {}),
|
|
2098
|
+
turnMode: collaborationTurnMode ?? 'ordinary',
|
|
2099
|
+
...(options?.kickoff ? { kickoff: options.kickoff } : {}),
|
|
2100
|
+
...(activeTurn?.protocol ? { protocol: activeTurn.protocol } : {}),
|
|
2101
|
+
...(collaborationGrounding ? { grounding: collaborationGrounding } : {}),
|
|
2102
|
+
}
|
|
2103
|
+
: undefined,
|
|
2104
|
+
collaborationOutcome,
|
|
2105
|
+
attentionSnapshot,
|
|
2106
|
+
...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
|
|
2107
|
+
...(collaborationCapabilities ? { collaborationCapabilities } : {}),
|
|
2108
|
+
...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
|
|
2109
|
+
}, usePublicDraftProgress || usePrivateCollaborationDraftProgress
|
|
2110
|
+
? {
|
|
2111
|
+
onProgress: (update) => {
|
|
2112
|
+
if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (usePublicDraftProgress) {
|
|
2116
|
+
this.ensureProgressDraft(channelId, activeTurn?.generation, activeTurn?.delivery).update(update.text);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
this.publishCollaborationDraft(channelId, activeTurn, update.text);
|
|
2120
|
+
},
|
|
2121
|
+
}
|
|
2122
|
+
: undefined);
|
|
2123
|
+
publicReplyText = resolvePublicReplyText(reply);
|
|
2124
|
+
visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
|
|
2125
|
+
const appliedAttentionSnapshot = this.applyAttentionUpdate(channelId, attentionSnapshot, reply.controlMalformed
|
|
2126
|
+
? undefined
|
|
2127
|
+
: (reply.control && 'attentionUpdate' in reply.control
|
|
2128
|
+
? reply.control.attentionUpdate
|
|
2129
|
+
: undefined), taskThreadContext, {
|
|
2130
|
+
observedAt: Date.now(),
|
|
2131
|
+
turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
|
|
2132
|
+
});
|
|
2133
|
+
if (!(await this.canAcceptCompletedTurnOutput(channelId, activeTurn))) {
|
|
2134
|
+
await this.discardDraft(channelId);
|
|
2135
|
+
if (activeTurn?.turnExecutionId) {
|
|
2136
|
+
this.clearCollaborationDraft(channelId, {
|
|
2137
|
+
generation: activeTurn.generation,
|
|
2138
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
return { reply, stale: true, completed: false };
|
|
2142
|
+
}
|
|
2143
|
+
if (silentTurn) {
|
|
2144
|
+
this.logger.debug('completed turn', {
|
|
2145
|
+
...turnContext,
|
|
2146
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2147
|
+
replyLength: publicReplyText.length,
|
|
2148
|
+
controlKind: reply?.control?.kind ?? null,
|
|
2149
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
2150
|
+
delivery: 'silent',
|
|
2151
|
+
});
|
|
2152
|
+
return {
|
|
2153
|
+
reply,
|
|
2154
|
+
deliveredMessageId: null,
|
|
2155
|
+
publicReplyText,
|
|
2156
|
+
visibleReplyBody,
|
|
2157
|
+
deliveryState: 'not-delivered',
|
|
2158
|
+
appliedAttentionSnapshot,
|
|
2159
|
+
stale: false,
|
|
2160
|
+
completed: true,
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
if (deferDelivery) {
|
|
2164
|
+
this.logger.debug('completed turn', {
|
|
2165
|
+
...turnContext,
|
|
2166
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2167
|
+
replyLength: publicReplyText.length,
|
|
2168
|
+
controlKind: reply?.control?.kind ?? null,
|
|
2169
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
2170
|
+
delivery: 'deferred',
|
|
2171
|
+
});
|
|
2172
|
+
return {
|
|
2173
|
+
reply,
|
|
2174
|
+
deliveredMessageId: null,
|
|
2175
|
+
publicReplyText,
|
|
2176
|
+
visibleReplyBody,
|
|
2177
|
+
deliveryState: 'not-delivered',
|
|
2178
|
+
appliedAttentionSnapshot,
|
|
2179
|
+
stale: false,
|
|
2180
|
+
completed: true,
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
if (usePublicDraftProgress) {
|
|
2184
|
+
const draft = this.progressDrafts.get(channelId);
|
|
2185
|
+
if (!this.started) {
|
|
2186
|
+
await draft?.discard().catch(() => { });
|
|
2187
|
+
this.progressDrafts.delete(channelId);
|
|
2188
|
+
return { reply, stale: true, completed: false };
|
|
2189
|
+
}
|
|
2190
|
+
const deliveredMessageId = await (draft ?? this.ensureProgressDraft(channelId, activeTurn?.generation, activeTurn?.delivery)).finalize(publicReplyText);
|
|
2191
|
+
this.progressDrafts.delete(channelId);
|
|
2192
|
+
this.logger.debug('completed turn', {
|
|
2193
|
+
...turnContext,
|
|
2194
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2195
|
+
replyLength: publicReplyText.length,
|
|
2196
|
+
controlKind: reply?.control?.kind ?? null,
|
|
2197
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
2198
|
+
delivery: 'draft',
|
|
2199
|
+
});
|
|
2200
|
+
return {
|
|
2201
|
+
reply,
|
|
2202
|
+
deliveredMessageId,
|
|
2203
|
+
publicReplyText,
|
|
2204
|
+
visibleReplyBody,
|
|
2205
|
+
deliveryState: deliveredMessageId ? 'draft-finalized' : 'not-delivered',
|
|
2206
|
+
appliedAttentionSnapshot,
|
|
2207
|
+
stale: false,
|
|
2208
|
+
completed: true,
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
else if (this.started && !this.controlPlaneClosed && hasVisibleText(publicReplyText)) {
|
|
2212
|
+
const posted = await this.borgee.postMessage({
|
|
2213
|
+
channelId,
|
|
2214
|
+
body: visibleReplyBody,
|
|
2215
|
+
replyToId: activeTurn?.delivery?.replyToId,
|
|
2216
|
+
});
|
|
2217
|
+
this.logger.debug('completed turn', {
|
|
2218
|
+
...turnContext,
|
|
2219
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2220
|
+
replyLength: publicReplyText.length,
|
|
2221
|
+
controlKind: reply?.control?.kind ?? null,
|
|
2222
|
+
controlMalformed: reply?.controlMalformed ?? false,
|
|
2223
|
+
delivery: 'post',
|
|
2224
|
+
});
|
|
2225
|
+
if (usePrivateCollaborationDraftProgress) {
|
|
2226
|
+
this.finalizeCollaborationDraft(channelId, activeTurn, posted.messageId, visibleReplyBody);
|
|
2227
|
+
}
|
|
2228
|
+
return {
|
|
2229
|
+
reply,
|
|
2230
|
+
deliveredMessageId: posted.messageId,
|
|
2231
|
+
publicReplyText,
|
|
2232
|
+
visibleReplyBody,
|
|
2233
|
+
deliveryState: 'posted',
|
|
2234
|
+
appliedAttentionSnapshot,
|
|
2235
|
+
stale: false,
|
|
2236
|
+
completed: true,
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
this.logger.debug('completed turn', {
|
|
2240
|
+
...turnContext,
|
|
2241
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2242
|
+
replyLength: publicReplyText.length,
|
|
2243
|
+
delivery: usePublicDraftProgress ? 'draft' : 'post',
|
|
2244
|
+
});
|
|
2245
|
+
if (usePrivateCollaborationDraftProgress && activeTurn?.turnExecutionId) {
|
|
2246
|
+
this.clearCollaborationDraft(channelId, {
|
|
2247
|
+
generation: activeTurn.generation,
|
|
2248
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
return {
|
|
2252
|
+
reply,
|
|
2253
|
+
deliveredMessageId: null,
|
|
2254
|
+
publicReplyText,
|
|
2255
|
+
visibleReplyBody,
|
|
2256
|
+
deliveryState: 'not-delivered',
|
|
2257
|
+
appliedAttentionSnapshot,
|
|
2258
|
+
stale: false,
|
|
2259
|
+
completed: true,
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
catch (error) {
|
|
2263
|
+
await this.discardDraft(channelId);
|
|
2264
|
+
if (activeTurn?.turnExecutionId) {
|
|
2265
|
+
this.clearCollaborationDraft(channelId, {
|
|
2266
|
+
generation: activeTurn.generation,
|
|
2267
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
this.logger.debugError('failed to generate or send reply', {
|
|
2271
|
+
...turnContext,
|
|
2272
|
+
error,
|
|
2273
|
+
});
|
|
2274
|
+
this.logger.error('failed to generate or send reply', {
|
|
2275
|
+
...turnContext,
|
|
2276
|
+
error: summarizeError(error),
|
|
2277
|
+
});
|
|
2278
|
+
return {
|
|
2279
|
+
reply,
|
|
2280
|
+
publicReplyText,
|
|
2281
|
+
visibleReplyBody,
|
|
2282
|
+
deliveryState: reply ? 'delivery-failed' : 'not-delivered',
|
|
2283
|
+
stale: activeTurn?.superseded === true,
|
|
2284
|
+
completed: false,
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
finally {
|
|
2288
|
+
stopTyping();
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
canAcceptTurnOutput(channelId, generation) {
|
|
2292
|
+
if (!this.started || this.controlPlaneClosed) {
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
if (generation == null) {
|
|
2296
|
+
return true;
|
|
2297
|
+
}
|
|
2298
|
+
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
2299
|
+
return (activeTurn?.generation === generation &&
|
|
2300
|
+
!activeTurn.superseded &&
|
|
2301
|
+
activeTurn.turnExecutionId != null);
|
|
2302
|
+
}
|
|
2303
|
+
async canAcceptCompletedTurnOutput(channelId, activeTurn) {
|
|
2304
|
+
if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
|
|
2305
|
+
return false;
|
|
2306
|
+
}
|
|
2307
|
+
if (!activeTurn?.protocol) {
|
|
2308
|
+
return true;
|
|
2309
|
+
}
|
|
2310
|
+
const channelState = this.collaborationChannels.get(channelId);
|
|
2311
|
+
if (!channelState?.protocol) {
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
if ((await this.canContinueSharedProtocol(channelId, channelState, channelState.protocol)) &&
|
|
2315
|
+
channelState.protocol.anchorMessageId === activeTurn.protocol.anchorMessageId) {
|
|
2316
|
+
return true;
|
|
2317
|
+
}
|
|
2318
|
+
this.supersedeActiveTurn(channelId, activeTurn);
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
async canContinueSharedProtocol(channelId, channelState, protocolState) {
|
|
2322
|
+
const sharedStatus = await this.readSharedProtocolStatus(channelId, protocolState.anchorMessageId);
|
|
2323
|
+
if (sharedStatus?.active !== false) {
|
|
2324
|
+
return protocolState.active;
|
|
2325
|
+
}
|
|
2326
|
+
protocolState.active = false;
|
|
2327
|
+
protocolState.finished = sharedStatus.finished;
|
|
2328
|
+
channelState.queue = channelState.queue.filter((message) => message.kind !== 'protocol-turn');
|
|
2329
|
+
channelState.recentTurn = undefined;
|
|
2330
|
+
this.clearProtocolFallback(channelState);
|
|
2331
|
+
return false;
|
|
2332
|
+
}
|
|
2333
|
+
enqueueProgressChannelTurn(channelId, task) {
|
|
2334
|
+
const previous = this.progressChannelQueues.get(channelId) ?? Promise.resolve();
|
|
2335
|
+
const next = previous
|
|
2336
|
+
.catch(() => { })
|
|
2337
|
+
.then(task)
|
|
2338
|
+
.finally(() => {
|
|
2339
|
+
if (this.progressChannelQueues.get(channelId) === next) {
|
|
2340
|
+
this.progressChannelQueues.delete(channelId);
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
this.progressChannelQueues.set(channelId, next);
|
|
2344
|
+
return next;
|
|
2345
|
+
}
|
|
2346
|
+
ensureProgressDraft(channelId, generation, creationMetadata) {
|
|
2347
|
+
let draft = this.progressDrafts.get(channelId);
|
|
2348
|
+
if (!draft) {
|
|
2349
|
+
draft = new DraftMessageController(this.borgee, channelId, () => !this.controlPlaneClosed && this.canAcceptTurnOutput(channelId, generation), this.logger, creationMetadata);
|
|
2350
|
+
this.progressDrafts.set(channelId, draft);
|
|
2351
|
+
}
|
|
2352
|
+
return draft;
|
|
2353
|
+
}
|
|
2354
|
+
publishCollaborationDraft(channelId, activeTurn, body) {
|
|
2355
|
+
if (!activeTurn?.turnExecutionId || !hasVisibleText(body)) {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const generation = activeTurn.generation;
|
|
2359
|
+
const existing = this.collaborationDrafts.get(channelId);
|
|
2360
|
+
if (existing &&
|
|
2361
|
+
(existing.generation !== generation || existing.turnExecutionId !== activeTurn.turnExecutionId)) {
|
|
2362
|
+
this.clearCollaborationDraft(channelId);
|
|
2363
|
+
}
|
|
2364
|
+
const next = this.collaborationDrafts.get(channelId);
|
|
2365
|
+
if (next?.clearTimer) {
|
|
2366
|
+
clearTimeout(next.clearTimer);
|
|
2367
|
+
next.clearTimer = null;
|
|
2368
|
+
}
|
|
2369
|
+
const snapshot = next?.snapshot ?? {
|
|
2370
|
+
draftId: randomUUID(),
|
|
2371
|
+
channelId,
|
|
2372
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2373
|
+
replyToId: activeTurn.delivery?.replyToId,
|
|
2374
|
+
body,
|
|
2375
|
+
status: 'streaming',
|
|
2376
|
+
updatedAt: Date.now(),
|
|
2377
|
+
};
|
|
2378
|
+
snapshot.body = body;
|
|
2379
|
+
snapshot.status = 'streaming';
|
|
2380
|
+
snapshot.updatedAt = Date.now();
|
|
2381
|
+
delete snapshot.finalMessageId;
|
|
2382
|
+
this.collaborationDrafts.set(channelId, {
|
|
2383
|
+
generation,
|
|
2384
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2385
|
+
snapshot,
|
|
2386
|
+
clearTimer: null,
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
finalizeCollaborationDraft(channelId, activeTurn, finalMessageId, finalBody) {
|
|
2390
|
+
if (!activeTurn?.turnExecutionId) {
|
|
2391
|
+
this.clearCollaborationDraft(channelId);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (!finalMessageId || !hasVisibleText(finalBody)) {
|
|
2395
|
+
this.clearCollaborationDraft(channelId, {
|
|
2396
|
+
generation: activeTurn.generation,
|
|
2397
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2398
|
+
});
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
const draft = this.collaborationDrafts.get(channelId);
|
|
2402
|
+
if (!draft ||
|
|
2403
|
+
draft.generation !== activeTurn.generation ||
|
|
2404
|
+
draft.turnExecutionId !== activeTurn.turnExecutionId) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (draft.clearTimer) {
|
|
2408
|
+
clearTimeout(draft.clearTimer);
|
|
2409
|
+
}
|
|
2410
|
+
draft.snapshot.body = finalBody;
|
|
2411
|
+
draft.snapshot.status = 'finalized';
|
|
2412
|
+
draft.snapshot.updatedAt = Date.now();
|
|
2413
|
+
draft.snapshot.finalMessageId = finalMessageId;
|
|
2414
|
+
draft.clearTimer = setTimeout(() => {
|
|
2415
|
+
this.clearCollaborationDraft(channelId, {
|
|
2416
|
+
generation: activeTurn.generation,
|
|
2417
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2418
|
+
});
|
|
2419
|
+
}, COLLABORATION_LATE_SEND_GRACE_MS);
|
|
2420
|
+
}
|
|
2421
|
+
clearCollaborationDraft(channelId, expected) {
|
|
2422
|
+
const draft = this.collaborationDrafts.get(channelId);
|
|
2423
|
+
if (!draft) {
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
if (expected &&
|
|
2427
|
+
(draft.generation !== expected.generation || draft.turnExecutionId !== expected.turnExecutionId)) {
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
if (draft.clearTimer) {
|
|
2431
|
+
clearTimeout(draft.clearTimer);
|
|
2432
|
+
}
|
|
2433
|
+
this.collaborationDrafts.delete(channelId);
|
|
2434
|
+
}
|
|
2435
|
+
readCollaborationDraft(input) {
|
|
2436
|
+
const draft = this.collaborationDrafts.get(input.channelId);
|
|
2437
|
+
if (!draft || draft.turnExecutionId !== input.turnExecutionId) {
|
|
2438
|
+
return null;
|
|
2439
|
+
}
|
|
2440
|
+
return { ...draft.snapshot };
|
|
2441
|
+
}
|
|
2442
|
+
async discardDraft(channelId) {
|
|
2443
|
+
const draft = this.progressDrafts.get(channelId);
|
|
2444
|
+
if (!draft) {
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
this.progressDrafts.delete(channelId);
|
|
2448
|
+
await draft.discard().catch(() => { });
|
|
2449
|
+
}
|
|
2450
|
+
supersedeActiveTurn(channelId, activeTurn) {
|
|
2451
|
+
if (activeTurn.superseded) {
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
this.recordCollaborationOutcome(channelId, {
|
|
2455
|
+
responseState: 'superseded',
|
|
2456
|
+
deliveryState: 'not-delivered',
|
|
2457
|
+
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
2458
|
+
});
|
|
2459
|
+
if (activeTurn.turnExecutionId) {
|
|
2460
|
+
this.clearCollaborationDraft(channelId, {
|
|
2461
|
+
generation: activeTurn.generation,
|
|
2462
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
activeTurn.superseded = true;
|
|
2466
|
+
activeTurn.turnExecutionId = null;
|
|
2467
|
+
void this.discardDraft(channelId);
|
|
2468
|
+
}
|
|
2469
|
+
invalidateChannelTurn(channelId) {
|
|
2470
|
+
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
2471
|
+
if (activeTurn) {
|
|
2472
|
+
activeTurn.turnExecutionId = null;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
rememberRecentTurn(channelState, activeTurn) {
|
|
2476
|
+
if (!activeTurn.turnExecutionId) {
|
|
2477
|
+
channelState.recentTurn = undefined;
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
channelState.recentTurn = {
|
|
2481
|
+
turnExecutionId: activeTurn.turnExecutionId,
|
|
2482
|
+
superseded: activeTurn.superseded,
|
|
2483
|
+
collaborationMessagesSent: activeTurn.collaborationMessagesSent,
|
|
2484
|
+
targetCooldowns: new Map(activeTurn.targetCooldowns),
|
|
2485
|
+
...(activeTurn.protocol ? { protocol: activeTurn.protocol } : {}),
|
|
2486
|
+
expiresAt: Date.now() + COLLABORATION_LATE_SEND_GRACE_MS,
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
resolveAuthorizedCollaborationTurn(channelState, turnExecutionId) {
|
|
2490
|
+
const activeTurn = channelState?.activeTurn;
|
|
2491
|
+
if (activeTurn &&
|
|
2492
|
+
!activeTurn.superseded &&
|
|
2493
|
+
activeTurn.turnExecutionId != null &&
|
|
2494
|
+
activeTurn.turnExecutionId === turnExecutionId) {
|
|
2495
|
+
return activeTurn;
|
|
2496
|
+
}
|
|
2497
|
+
const recentTurn = channelState?.recentTurn;
|
|
2498
|
+
if (!recentTurn) {
|
|
2499
|
+
return undefined;
|
|
2500
|
+
}
|
|
2501
|
+
if (recentTurn.expiresAt <= Date.now()) {
|
|
2502
|
+
channelState.recentTurn = undefined;
|
|
2503
|
+
return undefined;
|
|
2504
|
+
}
|
|
2505
|
+
if (!recentTurn.superseded && recentTurn.turnExecutionId === turnExecutionId) {
|
|
2506
|
+
return recentTurn;
|
|
2507
|
+
}
|
|
2508
|
+
return undefined;
|
|
2509
|
+
}
|
|
2510
|
+
updateAwaitingUserState(channelId, awaitingUser) {
|
|
2511
|
+
if (awaitingUser) {
|
|
2512
|
+
this.awaitingUserByChannel.set(channelId, awaitingUser);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
this.awaitingUserByChannel.delete(channelId);
|
|
2516
|
+
}
|
|
2517
|
+
recordBlockedSuppressionOutcome(channelId, channelState) {
|
|
2518
|
+
const blocked = channelState.blocked;
|
|
2519
|
+
if (!blocked) {
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
const existing = this.collaborationOutcomeByChannel.get(channelId);
|
|
2523
|
+
this.recordCollaborationOutcome(channelId, {
|
|
2524
|
+
responseState: 'blocked',
|
|
2525
|
+
deliveryState: existing?.deliveryState ?? 'not-delivered',
|
|
2526
|
+
wakeState: 'suppressed-agent',
|
|
2527
|
+
blockedDetails: {
|
|
2528
|
+
question: blocked.awaitingUser.question,
|
|
2529
|
+
...(blocked.awaitingUser.reason ? { reason: blocked.awaitingUser.reason } : {}),
|
|
2530
|
+
},
|
|
2531
|
+
turnExecutionId: channelState.activeTurn?.turnExecutionId
|
|
2532
|
+
?? channelState.recentTurn?.turnExecutionId
|
|
2533
|
+
?? existing?.turnExecutionId,
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
recordCollaborationOutcome(channelId, outcome) {
|
|
2537
|
+
this.collaborationOutcomeByChannel.set(channelId, {
|
|
2538
|
+
source: 'host-observed',
|
|
2539
|
+
responseState: outcome.responseState,
|
|
2540
|
+
deliveryState: outcome.deliveryState,
|
|
2541
|
+
...(outcome.wakeState ? { wakeState: outcome.wakeState } : {}),
|
|
2542
|
+
...(outcome.blockedDetails ? { blockedDetails: outcome.blockedDetails } : {}),
|
|
2543
|
+
observedAt: outcome.observedAt ?? Date.now(),
|
|
2544
|
+
...(outcome.turnExecutionId ? { turnExecutionId: outcome.turnExecutionId } : {}),
|
|
2545
|
+
});
|
|
2546
|
+
this.syncMissedCollaborationDiagnosticFromOutcome(channelId);
|
|
2547
|
+
}
|
|
2548
|
+
syncMissedCollaborationDiagnosticFromOutcome(channelId) {
|
|
2549
|
+
if (!this.collaborationCapabilitiesDiagnosticsEnabled) {
|
|
2550
|
+
this.missedCollaborationDiagnosticByChannel.delete(channelId);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
const outcome = this.collaborationOutcomeByChannel.get(channelId);
|
|
2554
|
+
if (!outcome) {
|
|
2555
|
+
this.missedCollaborationDiagnosticByChannel.delete(channelId);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
if (outcome.responseState === 'blocked'
|
|
2559
|
+
&& outcome.wakeState === 'waiting'
|
|
2560
|
+
&& outcome.blockedDetails) {
|
|
2561
|
+
this.updateMissedCollaborationDiagnostic(channelId, {
|
|
2562
|
+
stage: 'response',
|
|
2563
|
+
reason: 'blocked-awaiting-user',
|
|
2564
|
+
});
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (outcome.responseState === 'superseded') {
|
|
2568
|
+
this.updateMissedCollaborationDiagnostic(channelId, undefined);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
if (!this.collaborationOutcomeModelEnabled) {
|
|
2572
|
+
this.updateMissedCollaborationDiagnostic(channelId, undefined);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (outcome.deliveryState === 'delivery-failed') {
|
|
2576
|
+
this.updateMissedCollaborationDiagnostic(channelId, {
|
|
2577
|
+
stage: 'delivery',
|
|
2578
|
+
reason: 'delivery-failed',
|
|
2579
|
+
});
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
if (outcome.deliveryState === 'not-delivered' && outcome.responseState !== 'failed') {
|
|
2583
|
+
this.updateMissedCollaborationDiagnostic(channelId, {
|
|
2584
|
+
stage: 'delivery',
|
|
2585
|
+
reason: 'not-delivered',
|
|
2586
|
+
});
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
this.updateMissedCollaborationDiagnostic(channelId, undefined);
|
|
2590
|
+
}
|
|
2591
|
+
resolveVisibleTurnBody(activeTurn, reply, publicReplyText) {
|
|
2592
|
+
if (!activeTurn?.protocol || !this.shouldRelayProtocolReply(activeTurn, reply)) {
|
|
2593
|
+
return publicReplyText;
|
|
2594
|
+
}
|
|
2595
|
+
return appendVisibleBodyMention(publicReplyText, activeTurn.protocol.targetPeerId);
|
|
2596
|
+
}
|
|
2597
|
+
shouldRelayProtocolReply(activeTurn, reply) {
|
|
2598
|
+
const protocol = activeTurn.protocol;
|
|
2599
|
+
return (protocol != null &&
|
|
2600
|
+
reply.control?.kind === 'continue-to-peer' &&
|
|
2601
|
+
protocol.turnIndex < protocol.maxTurnCount);
|
|
2602
|
+
}
|
|
2603
|
+
async normalizeMessage(msg) {
|
|
2604
|
+
this.logger.debug('received channel event', {
|
|
2605
|
+
eventType: msg.type,
|
|
2606
|
+
messageType: msg.message_type,
|
|
2607
|
+
provider: this.config.agent.provider,
|
|
2608
|
+
agentName: this.config.agent.agentName,
|
|
2609
|
+
channelId: msg.channel_id,
|
|
2610
|
+
messageId: msg.message_id,
|
|
2611
|
+
});
|
|
2612
|
+
if (!isReplyTriggeringEventType(msg.type)) {
|
|
2613
|
+
this.logger.debug('ignored non-triggering channel event', {
|
|
2614
|
+
eventType: msg.type,
|
|
2615
|
+
messageType: msg.message_type,
|
|
2616
|
+
channelId: msg.channel_id,
|
|
2617
|
+
messageId: msg.message_id,
|
|
2618
|
+
});
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
2622
|
+
const authorId = String(msg.user_id ?? msg.sender_id ?? 'unknown');
|
|
2623
|
+
if (authorId === selfAgentId && msg.message_type !== 'task_assignment') {
|
|
2624
|
+
this.logger.debug('ignored self-authored message', {
|
|
2625
|
+
channelId: msg.channel_id,
|
|
2626
|
+
messageId: msg.message_id,
|
|
2627
|
+
});
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
const content = String(msg.content ?? msg.body ?? '').trim();
|
|
2631
|
+
if (!content) {
|
|
2632
|
+
this.logger.debug('ignored empty message', {
|
|
2633
|
+
channelId: msg.channel_id,
|
|
2634
|
+
messageId: msg.message_id,
|
|
2635
|
+
});
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
authorId,
|
|
2640
|
+
content,
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
async markAssignedTaskInProgress(threadId, content) {
|
|
2644
|
+
try {
|
|
2645
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
2646
|
+
const task = await waitForTaskForThread(this.borgee, threadId, {
|
|
2647
|
+
preferredTaskId: extractTaskIdFromTaskAssignmentContent(content),
|
|
2648
|
+
attempts: 10,
|
|
2649
|
+
delayMs: 250,
|
|
2650
|
+
});
|
|
2651
|
+
if (!task || task.status !== 'open' || task.assigneeId !== selfAgentId) {
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
await this.borgee.updateTask({
|
|
2655
|
+
taskId: task.id,
|
|
2656
|
+
status: 'in_progress',
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
catch (error) {
|
|
2660
|
+
this.logger.debugError('best-effort task auto-start failed', {
|
|
2661
|
+
channelId: threadId,
|
|
2662
|
+
error,
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async ensureSelfAgentId() {
|
|
2667
|
+
if (this.selfAgentId) {
|
|
2668
|
+
return this.selfAgentId;
|
|
2669
|
+
}
|
|
2670
|
+
if (!this.selfAgentIdPromise) {
|
|
2671
|
+
this.selfAgentIdPromise = this.borgee
|
|
2672
|
+
.getMe()
|
|
2673
|
+
.then((me) => {
|
|
2674
|
+
this.selfAgentId = me.id;
|
|
2675
|
+
this.selfAgentProfile = me;
|
|
2676
|
+
this.participantKindsByUserId.set(me.id, 'agent');
|
|
2677
|
+
this.participantDirectoryByUserId.set(me.id, {
|
|
2678
|
+
id: me.id,
|
|
2679
|
+
display_name: me.display_name,
|
|
2680
|
+
kind: 'agent',
|
|
2681
|
+
});
|
|
2682
|
+
return me.id;
|
|
2683
|
+
})
|
|
2684
|
+
.catch((error) => {
|
|
2685
|
+
this.selfAgentIdPromise = null;
|
|
2686
|
+
throw error;
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
return this.selfAgentIdPromise;
|
|
2690
|
+
}
|
|
2691
|
+
async classifyAuthor(authorId, forceRefreshOnMiss) {
|
|
2692
|
+
const cached = this.participantKindsByUserId.get(authorId);
|
|
2693
|
+
if (cached) {
|
|
2694
|
+
return cached === 'agent' ? 'agent' : 'human';
|
|
2695
|
+
}
|
|
2696
|
+
if (!this.participantDirectorySupported) {
|
|
2697
|
+
return this.inferAuthorKind(authorId);
|
|
2698
|
+
}
|
|
2699
|
+
const now = Date.now();
|
|
2700
|
+
const nextRefreshAt = this.participantRefreshBackoffByUserId.get(authorId) ?? 0;
|
|
2701
|
+
if (forceRefreshOnMiss || now >= nextRefreshAt) {
|
|
2702
|
+
await this.refreshParticipantDirectory();
|
|
2703
|
+
const refreshed = this.participantKindsByUserId.get(authorId);
|
|
2704
|
+
if (refreshed) {
|
|
2705
|
+
this.participantRefreshBackoffByUserId.delete(authorId);
|
|
2706
|
+
return refreshed === 'agent' ? 'agent' : 'human';
|
|
2707
|
+
}
|
|
2708
|
+
this.participantRefreshBackoffByUserId.set(authorId, now + BLOCKED_AUTHOR_REFRESH_BACKOFF_MS);
|
|
2709
|
+
}
|
|
2710
|
+
return 'unknown';
|
|
2711
|
+
}
|
|
2712
|
+
async refreshParticipantDirectory() {
|
|
2713
|
+
if (!this.participantDirectorySupported) {
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (!this.participantRefreshPromise) {
|
|
2717
|
+
this.participantRefreshPromise = this.borgee
|
|
2718
|
+
.listUsers()
|
|
2719
|
+
.then((users) => {
|
|
2720
|
+
const nextKinds = new Map();
|
|
2721
|
+
const nextDirectory = new Map();
|
|
2722
|
+
for (const user of users) {
|
|
2723
|
+
nextKinds.set(user.id, user.kind);
|
|
2724
|
+
nextDirectory.set(user.id, user);
|
|
2725
|
+
}
|
|
2726
|
+
if (this.selfAgentId) {
|
|
2727
|
+
nextKinds.set(this.selfAgentId, 'agent');
|
|
2728
|
+
nextDirectory.set(this.selfAgentId, {
|
|
2729
|
+
id: this.selfAgentId,
|
|
2730
|
+
display_name: this.selfAgentProfile?.display_name,
|
|
2731
|
+
kind: 'agent',
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
this.participantKindsByUserId.clear();
|
|
2735
|
+
this.participantDirectoryByUserId.clear();
|
|
2736
|
+
for (const [userId, kind] of nextKinds) {
|
|
2737
|
+
this.participantKindsByUserId.set(userId, kind);
|
|
2738
|
+
}
|
|
2739
|
+
for (const [userId, user] of nextDirectory) {
|
|
2740
|
+
this.participantDirectoryByUserId.set(userId, user);
|
|
2741
|
+
}
|
|
2742
|
+
})
|
|
2743
|
+
.catch((error) => {
|
|
2744
|
+
if (this.isUnsupportedSemanticOperation(error, 'list_users')) {
|
|
2745
|
+
this.participantDirectorySupported = false;
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
this.logger.error('failed to refresh participant directory', {
|
|
2749
|
+
error: summarizeError(error),
|
|
2750
|
+
});
|
|
2751
|
+
})
|
|
2752
|
+
.finally(() => {
|
|
2753
|
+
this.participantRefreshPromise = null;
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
await this.participantRefreshPromise;
|
|
2757
|
+
}
|
|
2758
|
+
async classifyBlockedWakeAuthor(authorId, blockedState) {
|
|
2759
|
+
void blockedState;
|
|
2760
|
+
return this.classifyAuthor(authorId, true);
|
|
2761
|
+
}
|
|
2762
|
+
async classifyQueuedBlockedWakeCandidate(message, blockedState) {
|
|
2763
|
+
void blockedState;
|
|
2764
|
+
return this.classifyAuthor(message.authorId, true);
|
|
2765
|
+
}
|
|
2766
|
+
inferAuthorKind(authorId) {
|
|
2767
|
+
if (this.selfAgentId === authorId) {
|
|
2768
|
+
return 'agent';
|
|
2769
|
+
}
|
|
2770
|
+
if (authorId.includes('@')) {
|
|
2771
|
+
return 'human';
|
|
2772
|
+
}
|
|
2773
|
+
if (authorId.startsWith('user-') || authorId.startsWith('human-')) {
|
|
2774
|
+
return 'human';
|
|
2775
|
+
}
|
|
2776
|
+
if (authorId.startsWith('agent-')) {
|
|
2777
|
+
return 'agent';
|
|
2778
|
+
}
|
|
2779
|
+
return 'unknown';
|
|
2780
|
+
}
|
|
2781
|
+
async buildCollaborationGrounding(authorId, incomingAuthorKind, activeTurn, kickoff) {
|
|
2782
|
+
if (!activeTurn?.protocol && !kickoff) {
|
|
2783
|
+
return undefined;
|
|
2784
|
+
}
|
|
2785
|
+
const selfAgentId = await this.ensureSelfAgentId();
|
|
2786
|
+
const peerId = activeTurn?.protocol?.targetPeerId ??
|
|
2787
|
+
kickoff?.participantIds.find((participantId) => participantId !== selfAgentId);
|
|
2788
|
+
const participantIds = kickoff?.participantIds ?? (peerId ? [selfAgentId, peerId] : [selfAgentId]);
|
|
2789
|
+
await this.hydratePromptIdentityCache([selfAgentId, authorId, ...participantIds]);
|
|
2790
|
+
return {
|
|
2791
|
+
self: this.resolvePromptIdentity(selfAgentId, 'agent'),
|
|
2792
|
+
incomingAuthor: this.resolvePromptIdentity(authorId, incomingAuthorKind ?? 'unknown'),
|
|
2793
|
+
...(peerId ? { peer: this.resolvePromptIdentity(peerId, 'agent') } : {}),
|
|
2794
|
+
participants: participantIds.map((participantId) => this.resolvePromptIdentity(participantId, 'agent')),
|
|
2795
|
+
...(kickoff ? { hostRecognizedKickoffCandidate: true } : {}),
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
async hydratePromptIdentityCache(userIds) {
|
|
2799
|
+
if (!this.participantDirectorySupported) {
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const unresolved = [...new Set(userIds)].filter((userId) => !this.participantDirectoryByUserId.has(userId));
|
|
2803
|
+
if (unresolved.length === 0) {
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
await this.refreshParticipantDirectory();
|
|
2807
|
+
}
|
|
2808
|
+
resolvePromptIdentity(userId, fallbackKind) {
|
|
2809
|
+
const directoryUser = this.participantDirectoryByUserId.get(userId);
|
|
2810
|
+
if (this.selfAgentId === userId) {
|
|
2811
|
+
return {
|
|
2812
|
+
id: userId,
|
|
2813
|
+
displayName: this.selfAgentProfile?.display_name ??
|
|
2814
|
+
directoryUser?.display_name ??
|
|
2815
|
+
this.config.agent.agentName,
|
|
2816
|
+
kind: 'agent',
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
id: userId,
|
|
2821
|
+
displayName: directoryUser?.display_name,
|
|
2822
|
+
kind: (directoryUser?.kind === 'user' ? 'human' : directoryUser?.kind) ?? fallbackKind,
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
isUnsupportedSemanticOperation(error, op) {
|
|
2826
|
+
if (!error || typeof error !== 'object') {
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
const code = 'code' in error ? error.code : undefined;
|
|
2830
|
+
const message = 'message' in error ? error.message : undefined;
|
|
2831
|
+
return (code === 'bpp.semantic_op_unknown' && typeof message === 'string' && message.includes(op));
|
|
2832
|
+
}
|
|
2833
|
+
authorizeCollaborationSend(input) {
|
|
2834
|
+
if (!this.collaborationEnabled) {
|
|
2835
|
+
return { ok: false, statusCode: 403, error: 'collaboration_not_enabled' };
|
|
2836
|
+
}
|
|
2837
|
+
const channelState = this.collaborationChannels.get(input.channelId);
|
|
2838
|
+
const authorizedTurn = this.resolveAuthorizedCollaborationTurn(channelState, input.turnExecutionId);
|
|
2839
|
+
if (!authorizedTurn) {
|
|
2840
|
+
return { ok: false, statusCode: 409, error: 'stale_turn_execution_id' };
|
|
2841
|
+
}
|
|
2842
|
+
if (authorizedTurn.protocol) {
|
|
2843
|
+
return { ok: false, statusCode: 403, error: 'protocol_managed_turn' };
|
|
2844
|
+
}
|
|
2845
|
+
if (authorizedTurn.collaborationMessagesSent >= COLLABORATION_SENDS_PER_TURN) {
|
|
2846
|
+
return { ok: false, statusCode: 429, error: 'collaboration_quota_exceeded' };
|
|
2847
|
+
}
|
|
2848
|
+
const targetKey = [
|
|
2849
|
+
input.replyToId ? `reply:${input.replyToId}` : '',
|
|
2850
|
+
input.mentions.length > 0 ? `mentions:${[...new Set(input.mentions)].sort().join(',')}` : '',
|
|
2851
|
+
]
|
|
2852
|
+
.filter((value) => value.length > 0)
|
|
2853
|
+
.join('|');
|
|
2854
|
+
const existingCooldownUntil = authorizedTurn.targetCooldowns.get(targetKey) ?? 0;
|
|
2855
|
+
if (existingCooldownUntil > Date.now()) {
|
|
2856
|
+
return { ok: false, statusCode: 429, error: 'collaboration_target_cooldown' };
|
|
2857
|
+
}
|
|
2858
|
+
const cooldownUntil = Date.now() + LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS;
|
|
2859
|
+
authorizedTurn.collaborationMessagesSent += 1;
|
|
2860
|
+
authorizedTurn.targetCooldowns.set(targetKey, cooldownUntil);
|
|
2861
|
+
let settled = false;
|
|
2862
|
+
return {
|
|
2863
|
+
ok: true,
|
|
2864
|
+
statusCode: 200,
|
|
2865
|
+
commit: () => {
|
|
2866
|
+
settled = true;
|
|
2867
|
+
},
|
|
2868
|
+
rollback: () => {
|
|
2869
|
+
if (settled) {
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
settled = true;
|
|
2873
|
+
authorizedTurn.collaborationMessagesSent = Math.max(0, authorizedTurn.collaborationMessagesSent - 1);
|
|
2874
|
+
if (authorizedTurn.targetCooldowns.get(targetKey) === cooldownUntil) {
|
|
2875
|
+
authorizedTurn.targetCooldowns.delete(targetKey);
|
|
2876
|
+
}
|
|
2877
|
+
},
|
|
2878
|
+
};
|
|
308
2879
|
}
|
|
309
2880
|
}
|