@borgee/agents-host 0.2.26 → 0.2.29

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.
@@ -4,12 +4,14 @@ import { dirname, join, resolve } from 'node:path';
4
4
  import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
5
5
  import { HostLogger, summarizeError } from './debug.js';
6
6
  import { createDurableCursorStore } from './durable-cursor-store.js';
7
- import { COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, TOKEN_BINDING_COMPATIBILITY_GATE, } from './compatibility-gates.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';
8
10
  import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS, } from './gateway/localhost-gateway.js';
9
- import { extractTaskIdFromTaskAssignmentContent } from './context/injection.js';
11
+ import { extractTaskIdFromTaskAssignmentContent, resolveChannelContextPayloadPath, } from './context/injection.js';
10
12
  import { AuthorizationAuditSink, } from './policy/authorization-audit.js';
11
13
  import { createProvider } from './providers/create-provider.js';
12
- import { resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
14
+ import { resolveAttentionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
13
15
  import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
14
16
  import { waitForTaskForThread } from './task-thread-resolution.js';
15
17
  const PRIVATE_STATE_ROOT_MODE = 0o700;
@@ -20,6 +22,7 @@ const PROTOCOL_KICKOFF_DECISION_TIMEOUT_MS = PROTOCOL_PROVIDER_TURN_TIMEOUT_MS;
20
22
  const PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS = 50;
21
23
  const PROTOCOL_KICKOFF_ROUND_LIMIT = 20;
22
24
  const COLLABORATION_LATE_SEND_GRACE_MS = 5_000;
25
+ const ORDINARY_HUMAN_EVENT_KINDS = new Set(['', 'message']);
23
26
  async function ensurePrivateStateRoot(path) {
24
27
  await mkdir(path, { recursive: true, mode: PRIVATE_STATE_ROOT_MODE });
25
28
  await chmod(path, PRIVATE_STATE_ROOT_MODE);
@@ -371,9 +374,17 @@ export class AgentsHost {
371
374
  provider;
372
375
  borgee;
373
376
  logger;
377
+ contextInjectionEnabled;
374
378
  collaborationEnabled;
379
+ collaborationOutcomeModelEnabled;
380
+ attentionFollowSemanticsEnabled;
381
+ taskThreadCollaborationContractEnabled;
382
+ collaborationCapabilitiesDiagnosticsEnabled;
375
383
  activeTurns = new Set();
376
384
  awaitingUserByChannel = new Map();
385
+ collaborationOutcomeByChannel = new Map();
386
+ attentionSnapshotByChannel = new Map();
387
+ missedCollaborationDiagnosticByChannel = new Map();
377
388
  progressChannelQueues = new Map();
378
389
  progressDrafts = new Map();
379
390
  collaborationDrafts = new Map();
@@ -393,6 +404,7 @@ export class AgentsHost {
393
404
  this.logger = new HostLogger(deps?.runtimeOptions);
394
405
  const compatibilityGates = resolveInternalCompatibilityGates();
395
406
  const contextInjectionGateEnabled = compatibilityGates.has(CONTEXT_INJECTION_COMPATIBILITY_GATE);
407
+ this.contextInjectionEnabled = contextInjectionGateEnabled;
396
408
  const skillRuntimeGateEnabled = compatibilityGates.has(SKILL_RUNTIME_COMPATIBILITY_GATE);
397
409
  const localhostGatewayGateEnabled = contextInjectionGateEnabled &&
398
410
  skillRuntimeGateEnabled &&
@@ -403,6 +415,14 @@ export class AgentsHost {
403
415
  compatibilityGates.has(COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE) &&
404
416
  policyAuditGateEnabled &&
405
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);
406
426
  this.runtime = createDefaultHostRuntime(config, {
407
427
  logger: this.logger,
408
428
  provider: deps?.provider,
@@ -481,6 +501,9 @@ export class AgentsHost {
481
501
  this.clearCollaborationDraft(channelId);
482
502
  }
483
503
  this.awaitingUserByChannel.clear();
504
+ this.collaborationOutcomeByChannel.clear();
505
+ this.attentionSnapshotByChannel.clear();
506
+ this.missedCollaborationDiagnosticByChannel.clear();
484
507
  this.controlPlaneClosed = true;
485
508
  await this.runtime.stop();
486
509
  await Promise.allSettled([...this.activeTurns]);
@@ -504,10 +527,25 @@ export class AgentsHost {
504
527
  if (!normalized) {
505
528
  return;
506
529
  }
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;
542
+ }
543
+ }
544
+ }
507
545
  if (msg.message_type === 'task_assignment') {
508
546
  await this.markAssignedTaskInProgress(msg.channel_id, normalized.content);
509
547
  }
510
- await this.runUngatedTurn(msg, normalized.authorId, normalized.content);
548
+ await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot);
511
549
  }
512
550
  async handleCollaborationMessage(msg) {
513
551
  const normalized = await this.normalizeMessage(msg);
@@ -519,8 +557,11 @@ export class AgentsHost {
519
557
  }
520
558
  const channelState = this.ensureCollaborationChannelState(msg.channel_id);
521
559
  await this.ensureProtocolStateLoaded(msg.channel_id, channelState);
560
+ await this.ensureAttentionSnapshotLoaded(msg.channel_id, channelState);
522
561
  const selfAgentId = await this.ensureSelfAgentId();
523
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);
524
565
  if (channelState.protocol?.active) {
525
566
  if (msg.message_id && authorKind === 'agent') {
526
567
  await this.handleActiveProtocolParticipantMessage(msg, normalized, channelState, channelState.protocol, selfAgentId);
@@ -542,6 +583,7 @@ export class AgentsHost {
542
583
  const blockedAuthorKind = await this.classifyBlockedWakeAuthor(normalized.authorId, channelState.blocked);
543
584
  if (blockedAuthorKind === 'agent') {
544
585
  channelState.blocked.suppressedAgentMessages += 1;
586
+ this.recordBlockedSuppressionOutcome(msg.channel_id, channelState);
545
587
  return;
546
588
  }
547
589
  if (blockedAuthorKind === 'unknown') {
@@ -550,6 +592,8 @@ export class AgentsHost {
550
592
  authorId: normalized.authorId,
551
593
  authorKind: 'unknown',
552
594
  content: normalized.content,
595
+ attentionSnapshot,
596
+ taskThreadContext,
553
597
  });
554
598
  const resumed = await this.tryResumeBlockedFromQueue(msg.channel_id, channelState);
555
599
  if (resumed) {
@@ -566,6 +610,8 @@ export class AgentsHost {
566
610
  authorId: normalized.authorId,
567
611
  authorKind: 'human',
568
612
  content: normalized.content,
613
+ attentionSnapshot,
614
+ taskThreadContext,
569
615
  wakeBlockedState,
570
616
  },
571
617
  ];
@@ -582,6 +628,9 @@ export class AgentsHost {
582
628
  }
583
629
  }
584
630
  }
631
+ if (this.isHumanWakeFiltered(msg, authorKind, attentionSnapshot)) {
632
+ return;
633
+ }
585
634
  if (authorKind !== 'agent' && channelState.activeTurn) {
586
635
  this.supersedeActiveTurn(msg.channel_id, channelState.activeTurn);
587
636
  }
@@ -593,6 +642,8 @@ export class AgentsHost {
593
642
  authorId: normalized.authorId,
594
643
  authorKind,
595
644
  content: normalized.content,
645
+ attentionSnapshot,
646
+ taskThreadContext,
596
647
  });
597
648
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
598
649
  }
@@ -681,6 +732,7 @@ export class AgentsHost {
681
732
  };
682
733
  }
683
734
  async evaluateIssuerProtocolKickoff(msg, normalized, protocolCandidate, pendingDecision) {
735
+ const priorMissedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(msg.channel_id);
684
736
  const executed = await this.executeTurn(msg, normalized.authorId, normalized.content, {
685
737
  kickoff: protocolCandidate,
686
738
  silent: true,
@@ -688,6 +740,11 @@ export class AgentsHost {
688
740
  if (executed.stale) {
689
741
  return null;
690
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);
691
748
  const reply = executed.reply;
692
749
  const rounds = reply?.control?.kind === 'start-protocol'
693
750
  ? this.validateProtocolKickoffRounds(reply.control.rounds)
@@ -958,6 +1015,7 @@ export class AgentsHost {
958
1015
  queue: [],
959
1016
  running: false,
960
1017
  protocolLoaded: false,
1018
+ attentionLoaded: false,
961
1019
  };
962
1020
  this.collaborationChannels.set(channelId, state);
963
1021
  }
@@ -1010,6 +1068,7 @@ export class AgentsHost {
1010
1068
  : await this.classifyQueuedBlockedWakeCandidate(queued, blocked);
1011
1069
  if (classified === 'agent') {
1012
1070
  blocked.suppressedAgentMessages += 1;
1071
+ this.recordBlockedSuppressionOutcome(channelId, channelState);
1013
1072
  continue;
1014
1073
  }
1015
1074
  if (classified === 'unknown') {
@@ -1037,6 +1096,12 @@ export class AgentsHost {
1037
1096
  await this.runProtocolManagedTurn(channelId, channelState, message);
1038
1097
  return;
1039
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
+ }
1040
1105
  channelState.generation += 1;
1041
1106
  const activeTurn = {
1042
1107
  generation: channelState.generation,
@@ -1051,24 +1116,66 @@ export class AgentsHost {
1051
1116
  const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
1052
1117
  activeTurn,
1053
1118
  incomingAuthorKind: message.authorKind,
1119
+ attentionSnapshot,
1120
+ taskThreadContext,
1054
1121
  });
1055
1122
  channelState.activeTurn = undefined;
1056
1123
  if (executed.stale) {
1057
1124
  return;
1058
1125
  }
1059
1126
  if (executed.completed) {
1127
+ await this.commitAttentionSnapshot(channelId, executed.appliedAttentionSnapshot ?? attentionSnapshot);
1060
1128
  this.rememberRecentTurn(channelState, activeTurn);
1061
1129
  }
1062
- if (executed.reply?.awaitingUser) {
1130
+ if (executed.completed && executed.reply?.awaitingUser) {
1063
1131
  channelState.blocked = {
1064
1132
  awaitingUser: executed.reply.awaitingUser,
1065
1133
  suppressedAgentMessages: 0,
1066
1134
  };
1067
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
+ });
1068
1169
  return;
1069
1170
  }
1070
1171
  channelState.blocked = undefined;
1071
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
+ });
1072
1179
  }
1073
1180
  async runProtocolManagedTurn(channelId, channelState, message) {
1074
1181
  const protocolState = channelState.protocol;
@@ -1083,6 +1190,10 @@ export class AgentsHost {
1083
1190
  const currentTurnIndex = protocolState.currentTurnIndex + 1;
1084
1191
  const peerId = this.otherProtocolParticipant(protocolState, selfAgentId);
1085
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);
1086
1197
  const activeTurn = {
1087
1198
  generation: channelState.generation,
1088
1199
  turnExecutionId: randomUUID(),
@@ -1108,25 +1219,40 @@ export class AgentsHost {
1108
1219
  const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
1109
1220
  activeTurn,
1110
1221
  incomingAuthorKind: message.authorKind,
1222
+ attentionSnapshot,
1223
+ taskThreadContext,
1111
1224
  });
1112
1225
  channelState.activeTurn = undefined;
1113
1226
  if (executed.stale || !protocolState.active) {
1114
1227
  return;
1115
1228
  }
1116
1229
  if (executed.completed) {
1230
+ await this.commitAttentionSnapshot(channelId, executed.appliedAttentionSnapshot ?? attentionSnapshot);
1117
1231
  this.rememberRecentTurn(channelState, activeTurn);
1118
1232
  }
1119
1233
  const deliveredMessageId = executed.deliveredMessageId ?? null;
1120
1234
  const visibleMessageId = deliveredMessageId ?? protocolState.lastProtocolMessageId;
1121
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
+ });
1122
1242
  this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
1123
1243
  return;
1124
1244
  }
1125
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
+ });
1126
1252
  this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
1127
1253
  return;
1128
1254
  }
1129
- if (executed.reply.control.kind === 'awaiting-user') {
1255
+ if (executed.completed && executed.reply.control.kind === 'awaiting-user') {
1130
1256
  channelState.blocked = {
1131
1257
  awaitingUser: executed.reply.awaitingUser ?? {
1132
1258
  question: executed.reply.control.question,
@@ -1135,10 +1261,40 @@ export class AgentsHost {
1135
1261
  suppressedAgentMessages: 0,
1136
1262
  };
1137
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
+ });
1138
1276
  this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
1139
1277
  return;
1140
1278
  }
1141
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);
1142
1298
  this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
1143
1299
  return;
1144
1300
  }
@@ -1146,6 +1302,12 @@ export class AgentsHost {
1146
1302
  this.finishProtocolState(channelId, channelState, protocolState);
1147
1303
  this.awaitingUserByChannel.delete(channelId);
1148
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
+ });
1149
1311
  return;
1150
1312
  }
1151
1313
  protocolState.currentTurnIndex = currentTurnIndex;
@@ -1156,12 +1318,24 @@ export class AgentsHost {
1156
1318
  this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
1157
1319
  this.awaitingUserByChannel.delete(channelId);
1158
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
+ });
1159
1327
  return;
1160
1328
  }
1161
1329
  if (currentTurnIndex >= protocolState.maxTurnCount) {
1162
1330
  this.finishProtocolState(channelId, channelState, protocolState, visibleMessageId);
1163
1331
  this.awaitingUserByChannel.delete(channelId);
1164
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
+ });
1165
1339
  return;
1166
1340
  }
1167
1341
  protocolState.expectedNextSpeakerId = peerId;
@@ -1172,6 +1346,12 @@ export class AgentsHost {
1172
1346
  }
1173
1347
  channelState.blocked = undefined;
1174
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
+ });
1175
1355
  }
1176
1356
  buildIncomingContent(message) {
1177
1357
  if (!message.wakeBlockedState) {
@@ -1495,7 +1675,9 @@ export class AgentsHost {
1495
1675
  type: 'message',
1496
1676
  };
1497
1677
  this.cancelProtocolState(channelId, channelState);
1498
- await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, protocolState.anchorContent);
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);
1499
1681
  }
1500
1682
  finishProtocolState(channelId, channelState, protocolState, lastMessageId) {
1501
1683
  protocolState.active = false;
@@ -1535,15 +1717,328 @@ export class AgentsHost {
1535
1717
  }
1536
1718
  return matched;
1537
1719
  }
1538
- async runUngatedTurn(msg, authorId, content) {
1539
- const executed = await this.executeTurn(msg, authorId, content);
1540
- if (executed.reply) {
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) {
1541
1995
  this.updateAwaitingUserState(msg.channel_id, executed.reply.awaitingUser);
1542
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
+ });
1543
2036
  }
1544
2037
  async executeTurn(msg, authorId, content, options) {
1545
2038
  const channelId = msg.channel_id;
1546
2039
  const activeTurn = options?.activeTurn;
2040
+ const attentionSnapshot = options?.attentionSnapshot;
2041
+ const taskThreadContext = options?.taskThreadContext ?? { state: 'inactive' };
1547
2042
  const silentTurn = options?.silent === true;
1548
2043
  const deferDelivery = options?.deferDelivery === true;
1549
2044
  const collaborationTurnMode = activeTurn?.protocol
@@ -1578,8 +2073,17 @@ export class AgentsHost {
1578
2073
  };
1579
2074
  const turnStartedAt = Date.now();
1580
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();
1581
2085
  try {
1582
- const reply = await this.provider.generateReply({
2086
+ reply = await this.provider.generateReply({
1583
2087
  agentName: this.config.agent.agentName,
1584
2088
  provider: this.config.agent.provider,
1585
2089
  channelId,
@@ -1597,6 +2101,11 @@ export class AgentsHost {
1597
2101
  ...(collaborationGrounding ? { grounding: collaborationGrounding } : {}),
1598
2102
  }
1599
2103
  : undefined,
2104
+ collaborationOutcome,
2105
+ attentionSnapshot,
2106
+ ...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
2107
+ ...(collaborationCapabilities ? { collaborationCapabilities } : {}),
2108
+ ...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
1600
2109
  }, usePublicDraftProgress || usePrivateCollaborationDraftProgress
1601
2110
  ? {
1602
2111
  onProgress: (update) => {
@@ -1611,8 +2120,16 @@ export class AgentsHost {
1611
2120
  },
1612
2121
  }
1613
2122
  : undefined);
1614
- const publicReplyText = resolvePublicReplyText(reply);
1615
- const visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
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
+ });
1616
2133
  if (!(await this.canAcceptCompletedTurnOutput(channelId, activeTurn))) {
1617
2134
  await this.discardDraft(channelId);
1618
2135
  if (activeTurn?.turnExecutionId) {
@@ -1637,6 +2154,8 @@ export class AgentsHost {
1637
2154
  deliveredMessageId: null,
1638
2155
  publicReplyText,
1639
2156
  visibleReplyBody,
2157
+ deliveryState: 'not-delivered',
2158
+ appliedAttentionSnapshot,
1640
2159
  stale: false,
1641
2160
  completed: true,
1642
2161
  };
@@ -1655,6 +2174,8 @@ export class AgentsHost {
1655
2174
  deliveredMessageId: null,
1656
2175
  publicReplyText,
1657
2176
  visibleReplyBody,
2177
+ deliveryState: 'not-delivered',
2178
+ appliedAttentionSnapshot,
1658
2179
  stale: false,
1659
2180
  completed: true,
1660
2181
  };
@@ -1681,6 +2202,8 @@ export class AgentsHost {
1681
2202
  deliveredMessageId,
1682
2203
  publicReplyText,
1683
2204
  visibleReplyBody,
2205
+ deliveryState: deliveredMessageId ? 'draft-finalized' : 'not-delivered',
2206
+ appliedAttentionSnapshot,
1684
2207
  stale: false,
1685
2208
  completed: true,
1686
2209
  };
@@ -1707,6 +2230,8 @@ export class AgentsHost {
1707
2230
  deliveredMessageId: posted.messageId,
1708
2231
  publicReplyText,
1709
2232
  visibleReplyBody,
2233
+ deliveryState: 'posted',
2234
+ appliedAttentionSnapshot,
1710
2235
  stale: false,
1711
2236
  completed: true,
1712
2237
  };
@@ -1728,6 +2253,8 @@ export class AgentsHost {
1728
2253
  deliveredMessageId: null,
1729
2254
  publicReplyText,
1730
2255
  visibleReplyBody,
2256
+ deliveryState: 'not-delivered',
2257
+ appliedAttentionSnapshot,
1731
2258
  stale: false,
1732
2259
  completed: true,
1733
2260
  };
@@ -1748,7 +2275,14 @@ export class AgentsHost {
1748
2275
  ...turnContext,
1749
2276
  error: summarizeError(error),
1750
2277
  });
1751
- return { stale: activeTurn?.superseded === true, completed: false };
2278
+ return {
2279
+ reply,
2280
+ publicReplyText,
2281
+ visibleReplyBody,
2282
+ deliveryState: reply ? 'delivery-failed' : 'not-delivered',
2283
+ stale: activeTurn?.superseded === true,
2284
+ completed: false,
2285
+ };
1752
2286
  }
1753
2287
  finally {
1754
2288
  stopTyping();
@@ -1917,6 +2451,11 @@ export class AgentsHost {
1917
2451
  if (activeTurn.superseded) {
1918
2452
  return;
1919
2453
  }
2454
+ this.recordCollaborationOutcome(channelId, {
2455
+ responseState: 'superseded',
2456
+ deliveryState: 'not-delivered',
2457
+ turnExecutionId: activeTurn.turnExecutionId ?? undefined,
2458
+ });
1920
2459
  if (activeTurn.turnExecutionId) {
1921
2460
  this.clearCollaborationDraft(channelId, {
1922
2461
  generation: activeTurn.generation,
@@ -1975,6 +2514,80 @@ export class AgentsHost {
1975
2514
  }
1976
2515
  this.awaitingUserByChannel.delete(channelId);
1977
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
+ }
1978
2591
  resolveVisibleTurnBody(activeTurn, reply, publicReplyText) {
1979
2592
  if (!activeTurn?.protocol || !this.shouldRelayProtocolReply(activeTurn, reply)) {
1980
2593
  return publicReplyText;