@borgee/agents-host 0.2.35 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +23 -27
  2. package/dist/agents-host.d.ts +28 -5
  3. package/dist/agents-host.js +274 -236
  4. package/dist/chat/chat-control-plane.d.ts +3 -0
  5. package/dist/chat/sdk-chat-control-plane.d.ts +4 -3
  6. package/dist/chat/sdk-chat-control-plane.js +6 -0
  7. package/dist/cli.js +1 -5
  8. package/dist/compatibility-gates.d.ts +4 -0
  9. package/dist/compatibility-gates.js +18 -1
  10. package/dist/context/claude-file-brief.d.ts +2 -0
  11. package/dist/context/claude-file-brief.js +83 -0
  12. package/dist/context/compaction.d.ts +20 -0
  13. package/dist/context/compaction.js +59 -0
  14. package/dist/context/injection.d.ts +44 -9
  15. package/dist/context/injection.js +360 -53
  16. package/dist/context/main-session-delegation.d.ts +1 -1
  17. package/dist/context/projection-strategy.d.ts +24 -0
  18. package/dist/context/projection-strategy.js +90 -0
  19. package/dist/context/prompt.d.ts +16 -1
  20. package/dist/context/prompt.js +475 -53
  21. package/dist/context/resolved-workspace.d.ts +2 -0
  22. package/dist/context/resolved-workspace.js +64 -0
  23. package/dist/context/skill-manual.d.ts +14 -0
  24. package/dist/context/skill-manual.js +21 -0
  25. package/dist/context/turn-preparation.d.ts +8 -2
  26. package/dist/context/turn-preparation.js +68 -17
  27. package/dist/gateway/localhost-gateway.js +18 -8
  28. package/dist/hosted-turn-content.d.ts +15 -0
  29. package/dist/hosted-turn-content.js +50 -0
  30. package/dist/managed-daemon.d.ts +3 -2
  31. package/dist/managed-daemon.js +198 -37
  32. package/dist/plugin-sdk.js +276 -359
  33. package/dist/plugin-sdk.js.map +4 -4
  34. package/dist/progress-to-activity.d.ts +16 -0
  35. package/dist/progress-to-activity.js +24 -0
  36. package/dist/projection-strategy-values.d.ts +4 -0
  37. package/dist/projection-strategy-values.js +28 -0
  38. package/dist/providers/acp-progress-collector.d.ts +44 -0
  39. package/dist/providers/acp-progress-collector.js +130 -0
  40. package/dist/providers/awaiting-user.d.ts +2 -3
  41. package/dist/providers/awaiting-user.js +5 -7
  42. package/dist/providers/claude/activity-metadata.d.ts +14 -0
  43. package/dist/providers/claude/activity-metadata.js +81 -0
  44. package/dist/providers/claude/adapter.d.ts +3 -1
  45. package/dist/providers/claude/adapter.js +10 -0
  46. package/dist/providers/claude/cli-client.d.ts +9 -1
  47. package/dist/providers/claude/cli-client.js +270 -126
  48. package/dist/providers/codex/adapter.d.ts +3 -1
  49. package/dist/providers/codex/adapter.js +10 -0
  50. package/dist/providers/codex/cli-client.d.ts +10 -2
  51. package/dist/providers/codex/cli-client.js +88 -104
  52. package/dist/providers/codex/project-doc.js +22 -37
  53. package/dist/providers/copilot/adapter.d.ts +3 -1
  54. package/dist/providers/copilot/adapter.js +10 -0
  55. package/dist/providers/copilot/cli-client.d.ts +9 -1
  56. package/dist/providers/copilot/cli-client.js +68 -85
  57. package/dist/providers/create-provider.d.ts +3 -1
  58. package/dist/providers/create-provider.js +36 -9
  59. package/dist/providers/provider-adapter.d.ts +35 -0
  60. package/dist/providers/provider-adapter.js +44 -1
  61. package/dist/state-paths.d.ts +10 -2
  62. package/dist/state-paths.js +25 -6
  63. package/dist/task-thread-resolution.d.ts +3 -2
  64. package/dist/types.d.ts +163 -8
  65. package/package.json +2 -2
  66. package/skills/borgee-agent/SKILL.md +127 -38
  67. package/skills/borgee-agent/references/errors.md +38 -0
  68. package/skills/borgee-agent/references/task-properties.md +33 -0
  69. package/skills/borgee-agent/scripts/borgee-agent.mjs +553 -0
  70. package/skills/borgee-agent/scripts/borgee-agent.py +547 -0
  71. package/dist/durable-cursor-store.d.ts +0 -5
  72. package/dist/durable-cursor-store.js +0 -7
  73. package/skills/borgee-agent/borgee-agent.mjs +0 -562
  74. package/skills/borgee-agent/borgee-agent.py +0 -469
@@ -3,19 +3,20 @@ import { chmod, link, mkdir, readFile, readdir, rename, unlink, writeFile } from
3
3
  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
- import { createDurableCursorStore } from './durable-cursor-store.js';
6
+ import { toReportedActivity } from './progress-to-activity.js';
7
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
8
  import { buildNormalizedAttentionSnapshot, parsePersistedAttentionSnapshot, } from './context/attention.js';
9
+ import { mergeCompactionSnapshot, parsePersistedCompactionSnapshot, } from './context/compaction.js';
9
10
  import { projectTaskThreadCollaborationContract } from './context/task-thread-collaboration.js';
10
11
  import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS, } from './gateway/localhost-gateway.js';
11
12
  import { extractTaskIdFromTaskAssignmentContent, resolveChannelContextPayloadPath, } from './context/injection.js';
12
13
  import { AuthorizationAuditSink, } from './policy/authorization-audit.js';
13
14
  import { createProvider } from './providers/create-provider.js';
14
- import { resolveAttentionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
15
+ import { resolveAttentionStatePath, resolveCompactionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
15
16
  import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
16
17
  import { waitForTaskForThread } from './task-thread-resolution.js';
18
+ import { buildHostedIncomingContentText, buildHostedTurnContentParts, extractHostedMessageAttachments, normalizeHostedMessageAttachment, } from './hosted-turn-content.js';
17
19
  const PRIVATE_STATE_ROOT_MODE = 0o700;
18
- const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
19
20
  const PROTOCOL_STATE_DIRNAME = 'protocol-state';
20
21
  const PROTOCOL_PROVIDER_TURN_TIMEOUT_MS = 120_000;
21
22
  const PROTOCOL_KICKOFF_DECISION_TIMEOUT_MS = PROTOCOL_PROVIDER_TURN_TIMEOUT_MS;
@@ -103,9 +104,6 @@ class DefaultHostRuntime {
103
104
  this.controlPlane =
104
105
  deps.borgee ??
105
106
  new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
106
- cursorStore: createDurableCursorStore({
107
- stateRootDir: config.stateRootDir,
108
- }),
109
107
  pluginId: `agents-host:${config.agent.provider}`,
110
108
  });
111
109
  this.gateway = createLocalhostGatewayController({
@@ -126,7 +124,9 @@ class DefaultHostRuntime {
126
124
  deps.provider ??
127
125
  createProvider({
128
126
  provider: config.agent.provider,
127
+ borgeeBaseUrl: config.borgeeBaseUrl,
129
128
  stateRootDir: config.stateRootDir,
129
+ agentApiKey: config.agent.agentApiKey,
130
130
  resolveStableAgentId: deps.resolveStableAgentId,
131
131
  claudeCommand: config.claudeCommand,
132
132
  claudeArgs: config.claudeArgs,
@@ -139,6 +139,7 @@ class DefaultHostRuntime {
139
139
  }, deps.logger, {
140
140
  compatibilityGates,
141
141
  authorizationAuditSink: this.authorizationAuditSink,
142
+ controlPlane: this.controlPlane,
142
143
  localhostGateway: localhostGatewayGateEnabled ? this.gateway.contextPublisher : undefined,
143
144
  });
144
145
  this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
@@ -255,113 +256,6 @@ function resolvePublicReplyText(reply) {
255
256
  }
256
257
  return reply.awaitingUser?.question ?? '';
257
258
  }
258
- function providerUsesDraftProgress(provider) {
259
- return provider === 'claude' || provider === 'codex' || provider === 'copilot';
260
- }
261
- class DraftMessageController {
262
- borgee;
263
- channelId;
264
- canWrite;
265
- logger;
266
- creationMetadata;
267
- messageId = null;
268
- currentBody = '';
269
- pendingBody = null;
270
- timer = null;
271
- sealed = false;
272
- writeChain = Promise.resolve();
273
- constructor(borgee, channelId, canWrite, logger, creationMetadata) {
274
- this.borgee = borgee;
275
- this.channelId = channelId;
276
- this.canWrite = canWrite;
277
- this.logger = logger;
278
- this.creationMetadata = creationMetadata;
279
- }
280
- update(text) {
281
- if (this.sealed || !hasVisibleText(text)) {
282
- return;
283
- }
284
- this.pendingBody = text;
285
- if (this.timer) {
286
- return;
287
- }
288
- this.timer = setTimeout(() => {
289
- this.timer = null;
290
- void this.flushPending().catch((error) => {
291
- this.logger.error('failed to flush draft progress', error);
292
- });
293
- }, STREAM_PROGRESS_EDIT_THROTTLE_MS);
294
- }
295
- cancelProgress() {
296
- if (this.timer) {
297
- clearTimeout(this.timer);
298
- this.timer = null;
299
- }
300
- this.sealed = true;
301
- }
302
- async finalize(finalText) {
303
- this.sealed = true;
304
- if (this.timer) {
305
- clearTimeout(this.timer);
306
- this.timer = null;
307
- }
308
- if (hasVisibleText(finalText)) {
309
- this.pendingBody = finalText;
310
- await this.flushPending();
311
- return this.messageId;
312
- }
313
- await this.enqueueWrite(async () => {
314
- if (!this.canWrite() || !this.messageId) {
315
- return;
316
- }
317
- await this.borgee.deleteMessage(this.messageId);
318
- this.messageId = null;
319
- this.currentBody = '';
320
- this.pendingBody = null;
321
- });
322
- return null;
323
- }
324
- async discard() {
325
- this.sealed = true;
326
- if (this.timer) {
327
- clearTimeout(this.timer);
328
- this.timer = null;
329
- }
330
- this.pendingBody = null;
331
- await this.enqueueWrite(async () => {
332
- if (!this.messageId) {
333
- return;
334
- }
335
- await this.borgee.deleteMessage(this.messageId);
336
- this.messageId = null;
337
- this.currentBody = '';
338
- });
339
- }
340
- async flushPending() {
341
- await this.enqueueWrite(async () => {
342
- const body = this.pendingBody;
343
- if (!this.canWrite() || !body || body === this.currentBody) {
344
- return;
345
- }
346
- if (this.messageId) {
347
- await this.borgee.editMessage(this.messageId, body);
348
- }
349
- else {
350
- const posted = await this.borgee.postMessage({
351
- channelId: this.channelId,
352
- body,
353
- replyToId: this.creationMetadata?.replyToId,
354
- });
355
- this.messageId = posted.messageId;
356
- }
357
- this.currentBody = body;
358
- });
359
- }
360
- enqueueWrite(operation) {
361
- this.writeChain = this.writeChain.then(operation, operation);
362
- return this.writeChain;
363
- }
364
- }
365
259
  /**
366
260
  * The registered task property agents-host writes to record which provider
367
261
  * session worked a task. It matches the server's registry key.
@@ -395,9 +289,12 @@ export class AgentsHost {
395
289
  */
396
290
  recordedAgentSessionByTask = new Map();
397
291
  attentionSnapshotByChannel = new Map();
292
+ compactionSnapshotByChannel = new Map();
293
+ activeCompactionByChannel = new Map();
294
+ compactionSnapshotLoadedChannels = new Set();
295
+ compactionSnapshotWriteQueueByChannel = new Map();
398
296
  missedCollaborationDiagnosticByChannel = new Map();
399
- progressChannelQueues = new Map();
400
- progressDrafts = new Map();
297
+ channelTurnQueues = new Map();
401
298
  collaborationDrafts = new Map();
402
299
  collaborationChannels = new Map();
403
300
  participantDirectoryByUserId = new Map();
@@ -505,20 +402,32 @@ export class AgentsHost {
505
402
  this.invalidateChannelTurn(channelId);
506
403
  this.clearProtocolFallback(this.collaborationChannels.get(channelId));
507
404
  }
508
- for (const draft of this.progressDrafts.values()) {
509
- draft.cancelProgress();
510
- }
511
405
  for (const channelId of [...this.collaborationDrafts.keys()]) {
512
406
  this.clearCollaborationDraft(channelId);
513
407
  }
514
408
  this.awaitingUserByChannel.clear();
515
409
  this.collaborationOutcomeByChannel.clear();
516
410
  this.attentionSnapshotByChannel.clear();
411
+ this.compactionSnapshotByChannel.clear();
412
+ this.activeCompactionByChannel.clear();
413
+ this.compactionSnapshotLoadedChannels.clear();
517
414
  this.missedCollaborationDiagnosticByChannel.clear();
518
415
  this.recordedAgentSessionByTask.clear();
519
416
  this.controlPlaneClosed = true;
520
- await this.runtime.stop();
417
+ const pendingCompactionSnapshotWrites = [...this.compactionSnapshotWriteQueueByChannel.values()];
418
+ let runtimeStopError;
419
+ try {
420
+ await this.runtime.stop();
421
+ }
422
+ catch (error) {
423
+ runtimeStopError = error;
424
+ }
521
425
  await Promise.allSettled([...this.activeTurns]);
426
+ await Promise.allSettled(pendingCompactionSnapshotWrites);
427
+ this.compactionSnapshotWriteQueueByChannel.clear();
428
+ if (runtimeStopError) {
429
+ throw runtimeStopError;
430
+ }
522
431
  }
523
432
  trackActiveTurn(task) {
524
433
  this.activeTurns.add(task);
@@ -529,9 +438,7 @@ export class AgentsHost {
529
438
  dispatchMessage(message) {
530
439
  const task = this.collaborationEnabled
531
440
  ? this.handleCollaborationMessage(message)
532
- : providerUsesDraftProgress(this.config.agent.provider)
533
- ? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleLegacyMessage(message))
534
- : this.handleLegacyMessage(message);
441
+ : this.enqueueChannelTurn(message.channel_id, () => this.handleLegacyMessage(message));
535
442
  this.trackActiveTurn(task);
536
443
  }
537
444
  async handleLegacyMessage(msg) {
@@ -559,7 +466,7 @@ export class AgentsHost {
559
466
  }
560
467
  }
561
468
  }
562
- await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot);
469
+ await this.runUngatedTurn(msg, normalized.authorId, normalized.content, taskThreadContext, attentionSnapshot, normalized.incomingParts);
563
470
  }
564
471
  async handleCollaborationMessage(msg) {
565
472
  const normalized = await this.normalizeMessage(msg);
@@ -608,6 +515,7 @@ export class AgentsHost {
608
515
  authorId: normalized.authorId,
609
516
  authorKind: 'unknown',
610
517
  content: normalized.content,
518
+ incomingParts: normalized.incomingParts,
611
519
  attentionSnapshot,
612
520
  taskThreadContext,
613
521
  });
@@ -626,6 +534,7 @@ export class AgentsHost {
626
534
  authorId: normalized.authorId,
627
535
  authorKind: 'human',
628
536
  content: normalized.content,
537
+ incomingParts: normalized.incomingParts,
629
538
  attentionSnapshot,
630
539
  taskThreadContext,
631
540
  wakeBlockedState,
@@ -658,6 +567,7 @@ export class AgentsHost {
658
567
  authorId: normalized.authorId,
659
568
  authorKind,
660
569
  content: normalized.content,
570
+ incomingParts: normalized.incomingParts,
661
571
  attentionSnapshot,
662
572
  taskThreadContext,
663
573
  });
@@ -752,6 +662,7 @@ export class AgentsHost {
752
662
  const executed = await this.executeTurn(msg, normalized.authorId, normalized.content, {
753
663
  kickoff: protocolCandidate,
754
664
  silent: true,
665
+ incomingParts: normalized.incomingParts,
755
666
  });
756
667
  if (executed.stale) {
757
668
  return null;
@@ -912,6 +823,7 @@ export class AgentsHost {
912
823
  authorId: normalized.authorId,
913
824
  authorKind: 'human',
914
825
  content: normalized.content,
826
+ incomingParts: normalized.incomingParts,
915
827
  });
916
828
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
917
829
  return;
@@ -924,6 +836,7 @@ export class AgentsHost {
924
836
  ownerAgentId: selfAgentId,
925
837
  anchorAuthorId: normalized.authorId,
926
838
  anchorContent: normalized.content,
839
+ anchorIncomingParts: normalized.incomingParts,
927
840
  anchorMessageId: msg.message_id ?? randomUUID(),
928
841
  participantIds: protocolRequest.participantIds,
929
842
  issuerId: protocolRequest.issuerId,
@@ -946,6 +859,7 @@ export class AgentsHost {
946
859
  authorId: normalized.authorId,
947
860
  authorKind: 'human',
948
861
  content: normalized.content,
862
+ incomingParts: normalized.incomingParts,
949
863
  });
950
864
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
951
865
  return;
@@ -1014,6 +928,7 @@ export class AgentsHost {
1014
928
  authorId: normalized.authorId,
1015
929
  authorKind: 'agent',
1016
930
  content: normalized.content,
931
+ incomingParts: normalized.incomingParts,
1017
932
  });
1018
933
  await this.ensureCollaborationProcessor(msg.channel_id, channelState);
1019
934
  }
@@ -1131,6 +1046,7 @@ export class AgentsHost {
1131
1046
  channelState.activeTurn = activeTurn;
1132
1047
  const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
1133
1048
  activeTurn,
1049
+ incomingParts: message.incomingParts,
1134
1050
  incomingAuthorKind: message.authorKind,
1135
1051
  attentionSnapshot,
1136
1052
  taskThreadContext,
@@ -1234,6 +1150,7 @@ export class AgentsHost {
1234
1150
  channelState.activeTurn = activeTurn;
1235
1151
  const executed = await this.executeTurn(message.raw, message.authorId, this.buildIncomingContent(message), {
1236
1152
  activeTurn,
1153
+ incomingParts: message.incomingParts,
1237
1154
  incomingAuthorKind: message.authorKind,
1238
1155
  attentionSnapshot,
1239
1156
  taskThreadContext,
@@ -1541,59 +1458,52 @@ export class AgentsHost {
1541
1458
  }
1542
1459
  async buildResumedProtocolMessage(channelId, protocolState) {
1543
1460
  if (protocolState.currentTurnIndex === 0) {
1461
+ const anchorIncomingParts = await this.ensureProtocolAnchorIncomingParts(channelId, protocolState);
1462
+ const anchorIncomingContent = buildHostedIncomingContentText(anchorIncomingParts);
1544
1463
  return {
1545
1464
  kind: 'protocol-turn',
1546
1465
  raw: {
1547
1466
  channel_id: channelId,
1548
1467
  message_id: protocolState.anchorMessageId,
1549
1468
  user_id: protocolState.anchorAuthorId,
1550
- content: protocolState.anchorContent,
1551
- body: protocolState.anchorContent,
1469
+ content: anchorIncomingContent,
1470
+ body: anchorIncomingContent,
1471
+ attachments: extractHostedMessageAttachments(anchorIncomingParts),
1552
1472
  type: 'message',
1553
1473
  },
1554
1474
  authorId: protocolState.anchorAuthorId,
1555
1475
  authorKind: 'human',
1556
- content: protocolState.anchorContent,
1476
+ content: anchorIncomingContent,
1477
+ incomingParts: anchorIncomingParts,
1557
1478
  };
1558
1479
  }
1559
1480
  if (!protocolState.lastProtocolMessageId) {
1560
1481
  return null;
1561
1482
  }
1562
- let before;
1563
- for (;;) {
1564
- const history = await this.borgee.readChannelHistory({
1565
- channelId,
1566
- before,
1567
- limit: 200,
1483
+ const priorMessage = await this.findChannelHistoryMessageById(channelId, protocolState.lastProtocolMessageId);
1484
+ if (priorMessage) {
1485
+ const incomingParts = buildHostedTurnContentParts({
1486
+ text: priorMessage.body,
1487
+ attachments: priorMessage.attachments,
1568
1488
  });
1569
- const priorMessage = history.find((entry) => entry.id === protocolState.lastProtocolMessageId);
1570
- if (priorMessage) {
1571
- return {
1572
- kind: 'protocol-turn',
1573
- raw: {
1574
- channel_id: channelId,
1575
- message_id: priorMessage.id,
1576
- user_id: priorMessage.authorId,
1577
- content: priorMessage.body,
1578
- body: priorMessage.body,
1579
- type: 'message',
1580
- created_at: priorMessage.createdAt,
1581
- },
1582
- authorId: priorMessage.authorId,
1583
- authorKind: 'agent',
1584
- content: priorMessage.body,
1585
- };
1586
- }
1587
- if (history.length < 200) {
1588
- break;
1589
- }
1590
- const oldestCreatedAt = history.reduce((oldest, entry) => {
1591
- return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
1592
- }, null);
1593
- if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
1594
- break;
1595
- }
1596
- before = oldestCreatedAt;
1489
+ const incomingContent = buildHostedIncomingContentText(incomingParts);
1490
+ return {
1491
+ kind: 'protocol-turn',
1492
+ raw: {
1493
+ channel_id: channelId,
1494
+ message_id: priorMessage.id,
1495
+ user_id: priorMessage.authorId,
1496
+ content: incomingContent,
1497
+ body: incomingContent,
1498
+ attachments: priorMessage.attachments,
1499
+ type: 'message',
1500
+ created_at: priorMessage.createdAt,
1501
+ },
1502
+ authorId: priorMessage.authorId,
1503
+ authorKind: 'agent',
1504
+ content: incomingContent,
1505
+ incomingParts,
1506
+ };
1597
1507
  }
1598
1508
  return null;
1599
1509
  }
@@ -1604,6 +1514,7 @@ export class AgentsHost {
1604
1514
  ownerAgentId,
1605
1515
  anchorAuthorId: protocolState.anchorAuthorId,
1606
1516
  anchorContent: protocolState.anchorContent,
1517
+ anchorIncomingParts: protocolState.anchorIncomingParts,
1607
1518
  anchorMessageId: protocolState.anchorMessageId,
1608
1519
  participantIds: protocolState.participantIds,
1609
1520
  issuerId: protocolState.issuerId,
@@ -1682,18 +1593,36 @@ export class AgentsHost {
1682
1593
  await this.persistProtocolState(channelId, protocolState);
1683
1594
  return;
1684
1595
  }
1596
+ const anchorIncomingParts = await this.ensureProtocolAnchorIncomingParts(channelId, protocolState);
1597
+ const anchorIncomingContent = buildHostedIncomingContentText(anchorIncomingParts);
1685
1598
  const fallbackMessage = {
1686
1599
  channel_id: channelId,
1687
1600
  message_id: protocolState.anchorMessageId,
1688
1601
  user_id: protocolState.anchorAuthorId,
1689
- content: protocolState.anchorContent,
1690
- body: protocolState.anchorContent,
1602
+ content: anchorIncomingContent,
1603
+ body: anchorIncomingContent,
1604
+ attachments: extractHostedMessageAttachments(anchorIncomingParts),
1691
1605
  type: 'message',
1692
1606
  };
1693
1607
  this.cancelProtocolState(channelId, channelState);
1694
1608
  const taskThreadContext = await this.buildTurnTaskThreadContext(fallbackMessage, channelId);
1695
1609
  const attentionSnapshot = this.getAttentionSnapshotForTurn(channelId, taskThreadContext);
1696
- await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, protocolState.anchorContent, taskThreadContext, attentionSnapshot);
1610
+ await this.runUngatedTurn(fallbackMessage, protocolState.anchorAuthorId, anchorIncomingContent, taskThreadContext, attentionSnapshot, anchorIncomingParts);
1611
+ }
1612
+ async ensureProtocolAnchorIncomingParts(channelId, protocolState) {
1613
+ if (protocolState.anchorIncomingParts) {
1614
+ return protocolState.anchorIncomingParts;
1615
+ }
1616
+ const anchorMessage = await this.findChannelHistoryMessageById(channelId, protocolState.anchorMessageId);
1617
+ if (!anchorMessage) {
1618
+ return buildHostedTurnContentParts({ text: protocolState.anchorContent });
1619
+ }
1620
+ const anchorIncomingParts = buildHostedTurnContentParts({
1621
+ text: protocolState.anchorContent,
1622
+ attachments: anchorMessage.attachments,
1623
+ });
1624
+ protocolState.anchorIncomingParts = anchorIncomingParts;
1625
+ return anchorIncomingParts;
1697
1626
  }
1698
1627
  finishProtocolState(channelId, channelState, protocolState, lastMessageId) {
1699
1628
  protocolState.active = false;
@@ -1733,15 +1662,125 @@ export class AgentsHost {
1733
1662
  }
1734
1663
  return matched;
1735
1664
  }
1665
+ async findChannelHistoryMessageById(channelId, messageId) {
1666
+ let before;
1667
+ for (;;) {
1668
+ const history = await this.borgee.readChannelHistory({
1669
+ channelId,
1670
+ before,
1671
+ limit: 200,
1672
+ });
1673
+ const matched = history.find((entry) => entry.id === messageId);
1674
+ if (matched) {
1675
+ return {
1676
+ ...matched,
1677
+ ...(matched.attachments
1678
+ ? { attachments: matched.attachments.map(normalizeHostedMessageAttachment) }
1679
+ : {}),
1680
+ };
1681
+ }
1682
+ if (history.length < 200) {
1683
+ break;
1684
+ }
1685
+ const oldestCreatedAt = history.reduce((oldest, entry) => {
1686
+ return oldest == null || entry.createdAt < oldest ? entry.createdAt : oldest;
1687
+ }, null);
1688
+ if (oldestCreatedAt == null || (before != null && oldestCreatedAt >= before)) {
1689
+ break;
1690
+ }
1691
+ before = oldestCreatedAt;
1692
+ }
1693
+ return null;
1694
+ }
1736
1695
  resolveAttentionStatePath(channelId) {
1737
1696
  return resolveAttentionStatePath(this.config.stateRootDir, channelId);
1738
1697
  }
1698
+ resolveCompactionStatePath(channelId) {
1699
+ return resolveCompactionStatePath(this.config.stateRootDir, channelId);
1700
+ }
1739
1701
  buildDefaultAttentionSnapshot(options) {
1740
1702
  return buildNormalizedAttentionSnapshot({
1741
1703
  observedAt: options?.observedAt,
1742
1704
  turnExecutionId: options?.turnExecutionId,
1743
1705
  });
1744
1706
  }
1707
+ async ensureCompactionSnapshotLoaded(channelId) {
1708
+ if (this.compactionSnapshotLoadedChannels.has(channelId)) {
1709
+ return;
1710
+ }
1711
+ this.compactionSnapshotLoadedChannels.add(channelId);
1712
+ try {
1713
+ const content = await readFile(this.resolveCompactionStatePath(channelId), 'utf8');
1714
+ const snapshot = parsePersistedCompactionSnapshot(JSON.parse(content));
1715
+ if (snapshot) {
1716
+ this.compactionSnapshotByChannel.set(channelId, snapshot);
1717
+ }
1718
+ }
1719
+ catch (error) {
1720
+ const code = error.code;
1721
+ if (code !== 'ENOENT') {
1722
+ this.logger.error('failed to load compaction state', {
1723
+ channelId,
1724
+ error: summarizeError(error),
1725
+ });
1726
+ }
1727
+ this.compactionSnapshotByChannel.delete(channelId);
1728
+ }
1729
+ }
1730
+ async commitCompactionSnapshot(channelId, snapshot) {
1731
+ if (snapshot.stage === 'started') {
1732
+ return;
1733
+ }
1734
+ await writePrivateJsonFile(this.resolveCompactionStatePath(channelId), snapshot);
1735
+ }
1736
+ enqueueCompactionSnapshotCommit(channelId, snapshot) {
1737
+ const previous = this.compactionSnapshotWriteQueueByChannel.get(channelId) ?? Promise.resolve();
1738
+ const next = previous
1739
+ .catch(() => { })
1740
+ .then(() => this.commitCompactionSnapshot(channelId, snapshot));
1741
+ this.compactionSnapshotWriteQueueByChannel.set(channelId, next);
1742
+ void next.catch((error) => {
1743
+ this.logger.error('failed to persist compaction state', {
1744
+ channelId,
1745
+ error: summarizeError(error),
1746
+ });
1747
+ }).finally(() => {
1748
+ if (this.compactionSnapshotWriteQueueByChannel.get(channelId) === next) {
1749
+ this.compactionSnapshotWriteQueueByChannel.delete(channelId);
1750
+ }
1751
+ });
1752
+ }
1753
+ noteCompactionProgress(channelId, update, options) {
1754
+ const baseline = update.stage === 'started'
1755
+ ? undefined
1756
+ : this.activeCompactionByChannel.get(channelId)
1757
+ ?? this.compactionSnapshotByChannel.get(channelId);
1758
+ const nextSnapshot = mergeCompactionSnapshot(baseline, {
1759
+ provider: update.provider,
1760
+ stage: update.stage,
1761
+ observedAt: Date.now(),
1762
+ turnExecutionId: options?.turnExecutionId,
1763
+ usedTokens: update.usedTokens,
1764
+ contextWindowTokens: update.contextWindowTokens,
1765
+ failureReason: update.failureReason,
1766
+ });
1767
+ this.logger.debug('provider compaction update', {
1768
+ channelId,
1769
+ provider: nextSnapshot.provider,
1770
+ stage: nextSnapshot.stage,
1771
+ usedTokens: nextSnapshot.usedTokens,
1772
+ contextWindowTokens: nextSnapshot.contextWindowTokens,
1773
+ failureReason: nextSnapshot.failureReason,
1774
+ turnExecutionId: nextSnapshot.turnExecutionId,
1775
+ });
1776
+ if (nextSnapshot.stage === 'started') {
1777
+ this.activeCompactionByChannel.set(channelId, nextSnapshot);
1778
+ return;
1779
+ }
1780
+ this.activeCompactionByChannel.delete(channelId);
1781
+ this.compactionSnapshotByChannel.set(channelId, nextSnapshot);
1782
+ this.enqueueCompactionSnapshotCommit(channelId, nextSnapshot);
1783
+ }
1745
1784
  /**
1746
1785
  * Record which provider session worked this task, as agent.session_id.
1747
1786
  *
@@ -1997,14 +2036,15 @@ export class AgentsHost {
1997
2036
  taskThreadContextActive: taskThreadContext.state === 'active',
1998
2037
  });
1999
2038
  }
2000
- resolveCollaborationCapabilityDeclarationForTurn() {
2039
+ resolveCollaborationCapabilityDeclarationForTurn(taskThreadContext) {
2001
2040
  if (!this.collaborationCapabilitiesDiagnosticsEnabled) {
2002
2041
  return undefined;
2003
2042
  }
2004
2043
  return {
2005
2044
  ...(this.collaborationOutcomeModelEnabled ? { collaborationOutcome: true } : {}),
2006
2045
  ...(this.attentionFollowSemanticsEnabled ? { attentionSnapshot: true } : {}),
2007
- ...(this.taskThreadCollaborationContractEnabled
2046
+ ...(this.contextInjectionEnabled ? { compactionSnapshot: true } : {}),
2047
+ ...(this.taskThreadCollaborationContractEnabled && taskThreadContext.state === 'active'
2008
2048
  ? { taskThreadCollaborationContract: true }
2009
2049
  : {}),
2010
2050
  missedCollaborationDiagnostic: true,
@@ -2037,10 +2077,11 @@ export class AgentsHost {
2037
2077
  reason: 'wake-suppressed-attention-policy',
2038
2078
  });
2039
2079
  }
2040
- async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot) {
2080
+ async runUngatedTurn(msg, authorId, content, taskThreadContext, attentionSnapshot, incomingParts) {
2041
2081
  const executed = await this.executeTurn(msg, authorId, content, {
2042
2082
  attentionSnapshot,
2043
2083
  taskThreadContext,
2084
+ incomingParts,
2044
2085
  });
2045
2086
  if (executed.completed && executed.reply) {
2046
2087
  this.updateAwaitingUserState(msg.channel_id, executed.reply.awaitingUser);
@@ -2100,18 +2141,12 @@ export class AgentsHost {
2100
2141
  const turnExecutionId = activeTurn?.turnExecutionId ?? null;
2101
2142
  const stopTyping = silentTurn ? () => { } : this.borgee.startTyping(channelId);
2102
2143
  const collaborationGrounding = await this.buildCollaborationGrounding(authorId, options?.incomingAuthorKind, activeTurn, options?.kickoff);
2103
- const usePublicDraftProgress = !silentTurn &&
2104
- !deferDelivery &&
2105
- !this.collaborationEnabled &&
2106
- options?.incomingAuthorKind !== 'agent' &&
2107
- providerUsesDraftProgress(this.config.agent.provider) &&
2108
- activeTurn?.protocol === undefined;
2109
- const usePrivateCollaborationDraftProgress = !silentTurn &&
2144
+ const publishesCollaborationDraft = !silentTurn &&
2110
2145
  !deferDelivery &&
2111
2146
  this.collaborationEnabled &&
2112
2147
  turnExecutionId != null &&
2113
- providerUsesDraftProgress(this.config.agent.provider) &&
2114
2148
  activeTurn?.protocol === undefined;
2149
+ const observeProviderProgress = publishesCollaborationDraft || this.config.agent.provider === 'claude';
2115
2150
  const turnContext = {
2116
2151
  eventType: msg.type,
2117
2152
  channelId,
@@ -2132,7 +2167,15 @@ export class AgentsHost {
2132
2167
  ? this.collaborationOutcomeByChannel.get(channelId)
2133
2168
  : undefined;
2134
2169
  const missedCollaborationDiagnostic = this.resolveMissedCollaborationDiagnosticForTurn(channelId);
2135
- const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn();
2170
+ const collaborationCapabilities = this.resolveCollaborationCapabilityDeclarationForTurn(taskThreadContext);
2171
+ const incomingParts = options?.incomingParts
2172
+ ?? buildHostedTurnContentParts({
2173
+ text: String(msg.content ?? msg.body ?? content),
2174
+ attachments: msg.attachments,
2175
+ });
2176
+ const activityReporter = this.beginTurnActivity(channelId, activeTurn?.generation);
2177
+ await this.ensureCompactionSnapshotLoaded(channelId);
2178
+ const compactionSnapshot = this.compactionSnapshotByChannel.get(channelId);
2136
2179
  try {
2137
2180
  reply = await this.provider.generateReply({
2138
2181
  agentName: this.config.agent.agentName,
@@ -2140,6 +2183,7 @@ export class AgentsHost {
2140
2183
  channelId,
2141
2184
  incomingAuthorId: authorId,
2142
2185
  incomingContent: content,
2186
+ incomingParts,
2143
2187
  incomingEventKind: msg.type,
2144
2188
  incomingMessageType: msg.message_type,
2145
2189
  collaboration: turnExecutionId || options?.kickoff
@@ -2154,17 +2198,30 @@ export class AgentsHost {
2154
2198
  : undefined,
2155
2199
  collaborationOutcome,
2156
2200
  attentionSnapshot,
2201
+ compactionSnapshot,
2157
2202
  ...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
2158
2203
  ...(collaborationCapabilities ? { collaborationCapabilities } : {}),
2159
2204
  ...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
2160
- }, usePublicDraftProgress || usePrivateCollaborationDraftProgress
2205
+ }, observeProviderProgress
2161
2206
  ? {
2162
2207
  onProgress: (update) => {
2163
2208
  if (!this.canAcceptTurnOutput(channelId, activeTurn?.generation)) {
2164
2209
  return;
2165
2210
  }
2166
- if (usePublicDraftProgress) {
2167
- this.ensureProgressDraft(channelId, activeTurn?.generation, activeTurn?.delivery).update(update.text);
2211
+ if (update.type === 'compaction') {
2212
+ this.noteCompactionProgress(channelId, update.compaction, {
2213
+ turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
2214
+ });
2215
+ return;
2216
+ }
2217
+ const reportedActivity = toReportedActivity(update);
2218
+ if (reportedActivity) {
2219
+ activityReporter.progress(reportedActivity);
2220
+ }
2221
+ if (update.type !== 'text' || update.stream !== 'answer') {
2222
+ return;
2223
+ }
2224
+ if (!publishesCollaborationDraft) {
2168
2225
  return;
2169
2226
  }
2170
2227
  this.publishCollaborationDraft(channelId, activeTurn, update.text);
@@ -2187,7 +2244,6 @@ export class AgentsHost {
2187
2244
  turnExecutionId: activeTurn?.turnExecutionId ?? undefined,
2188
2245
  });
2189
2246
  if (!(await this.canAcceptCompletedTurnOutput(channelId, activeTurn))) {
2190
- await this.discardDraft(channelId);
2191
2247
  if (activeTurn?.turnExecutionId) {
2192
2248
  this.clearCollaborationDraft(channelId, {
2193
2249
  generation: activeTurn.generation,
@@ -2236,35 +2292,7 @@ export class AgentsHost {
2236
2292
  completed: true,
2237
2293
  };
2238
2294
  }
2239
- if (usePublicDraftProgress) {
2240
- const draft = this.progressDrafts.get(channelId);
2241
- if (!this.started) {
2242
- await draft?.discard().catch(() => { });
2243
- this.progressDrafts.delete(channelId);
2244
- return { reply, stale: true, completed: false };
2245
- }
2246
- const deliveredMessageId = await (draft ?? this.ensureProgressDraft(channelId, activeTurn?.generation, activeTurn?.delivery)).finalize(publicReplyText);
2247
- this.progressDrafts.delete(channelId);
2248
- this.logger.debug('completed turn', {
2249
- ...turnContext,
2250
- durationMs: Date.now() - turnStartedAt,
2251
- replyLength: publicReplyText.length,
2252
- controlKind: reply?.control?.kind ?? null,
2253
- controlMalformed: reply?.controlMalformed ?? false,
2254
- delivery: 'draft',
2255
- });
2256
- return {
2257
- reply,
2258
- deliveredMessageId,
2259
- publicReplyText,
2260
- visibleReplyBody,
2261
- deliveryState: deliveredMessageId ? 'draft-finalized' : 'not-delivered',
2262
- appliedAttentionSnapshot,
2263
- stale: false,
2264
- completed: true,
2265
- };
2266
- }
2267
- else if (this.started && !this.controlPlaneClosed && hasVisibleText(publicReplyText)) {
2295
+ if (this.started && !this.controlPlaneClosed && hasVisibleText(publicReplyText)) {
2268
2296
  const posted = await this.borgee.postMessage({
2269
2297
  channelId,
2270
2298
  body: visibleReplyBody,
@@ -2278,7 +2306,7 @@ export class AgentsHost {
2278
2306
  controlMalformed: reply?.controlMalformed ?? false,
2279
2307
  delivery: 'post',
2280
2308
  });
2281
- if (usePrivateCollaborationDraftProgress) {
2309
+ if (publishesCollaborationDraft) {
2282
2310
  this.finalizeCollaborationDraft(channelId, activeTurn, posted.messageId, visibleReplyBody);
2283
2311
  }
2284
2312
  return {
@@ -2296,9 +2324,9 @@ export class AgentsHost {
2296
2324
  ...turnContext,
2297
2325
  durationMs: Date.now() - turnStartedAt,
2298
2326
  replyLength: publicReplyText.length,
2299
- delivery: usePublicDraftProgress ? 'draft' : 'post',
2327
+ delivery: 'post',
2300
2328
  });
2301
- if (usePrivateCollaborationDraftProgress && activeTurn?.turnExecutionId) {
2329
+ if (publishesCollaborationDraft && activeTurn?.turnExecutionId) {
2302
2330
  this.clearCollaborationDraft(channelId, {
2303
2331
  generation: activeTurn.generation,
2304
2332
  turnExecutionId: activeTurn.turnExecutionId,
@@ -2316,7 +2344,6 @@ export class AgentsHost {
2316
2344
  };
2317
2345
  }
2318
2346
  catch (error) {
2319
- await this.discardDraft(channelId);
2320
2347
  if (activeTurn?.turnExecutionId) {
2321
2348
  this.clearCollaborationDraft(channelId, {
2322
2349
  generation: activeTurn.generation,
@@ -2342,8 +2369,31 @@ export class AgentsHost {
2342
2369
  }
2343
2370
  finally {
2344
2371
  stopTyping();
2372
+ activityReporter.end();
2345
2373
  }
2346
2374
  }
2375
+ /**
2376
+ * Opens this turn's report on the activity rail.
2377
+ *
2378
+ * The generation is captured here, where the turn's own is, and every report
2379
+ * is checked against the channel's current turn before it leaves. That is what
2380
+ * keeps a superseded turn from announcing a boundary for a turn that no longer
2381
+ * exists — including its ending, which a window opened before it was replaced
2382
+ * would otherwise carry out on its behalf.
2383
+ *
2384
+ * Every turn reports, including the silent and deferred ones that deliver
2385
+ * nothing to the channel. The rail writes nothing anywhere, and the question
2386
+ * it answers — whether anything is happening in this channel — is worth the
2387
+ * same answer for a turn that keeps its reply to itself.
2388
+ */
2389
+ beginTurnActivity(channelId, generation) {
2390
+ const reporter = this.borgee.reportTurnActivity({
2391
+ channelId,
2392
+ canReport: () => this.canAcceptTurnOutput(channelId, generation),
2393
+ });
2394
+ reporter.start();
2395
+ return reporter;
2396
+ }
2347
2397
  canAcceptTurnOutput(channelId, generation) {
2348
2398
  if (!this.started || this.controlPlaneClosed) {
2349
2399
  return false;
@@ -2386,27 +2436,19 @@ export class AgentsHost {
2386
2436
  this.clearProtocolFallback(channelState);
2387
2437
  return false;
2388
2438
  }
2389
- enqueueProgressChannelTurn(channelId, task) {
2390
- const previous = this.progressChannelQueues.get(channelId) ?? Promise.resolve();
2439
+ enqueueChannelTurn(channelId, task) {
2440
+ const previous = this.channelTurnQueues.get(channelId) ?? Promise.resolve();
2391
2441
  const next = previous
2392
2442
  .catch(() => { })
2393
2443
  .then(task)
2394
2444
  .finally(() => {
2395
- if (this.progressChannelQueues.get(channelId) === next) {
2396
- this.progressChannelQueues.delete(channelId);
2445
+ if (this.channelTurnQueues.get(channelId) === next) {
2446
+ this.channelTurnQueues.delete(channelId);
2397
2447
  }
2398
2448
  });
2399
- this.progressChannelQueues.set(channelId, next);
2449
+ this.channelTurnQueues.set(channelId, next);
2400
2450
  return next;
2401
2451
  }
2402
- ensureProgressDraft(channelId, generation, creationMetadata) {
2403
- let draft = this.progressDrafts.get(channelId);
2404
- if (!draft) {
2405
- draft = new DraftMessageController(this.borgee, channelId, () => !this.controlPlaneClosed && this.canAcceptTurnOutput(channelId, generation), this.logger, creationMetadata);
2406
- this.progressDrafts.set(channelId, draft);
2407
- }
2408
- return draft;
2409
- }
2410
2452
  publishCollaborationDraft(channelId, activeTurn, body) {
2411
2453
  if (!activeTurn?.turnExecutionId || !hasVisibleText(body)) {
2412
2454
  return;
@@ -2495,14 +2537,6 @@ export class AgentsHost {
2495
2537
  }
2496
2538
  return { ...draft.snapshot };
2497
2539
  }
2498
- async discardDraft(channelId) {
2499
- const draft = this.progressDrafts.get(channelId);
2500
- if (!draft) {
2501
- return;
2502
- }
2503
- this.progressDrafts.delete(channelId);
2504
- await draft.discard().catch(() => { });
2505
- }
2506
2540
  supersedeActiveTurn(channelId, activeTurn) {
2507
2541
  if (activeTurn.superseded) {
2508
2542
  return;
@@ -2520,7 +2554,6 @@ export class AgentsHost {
2520
2554
  }
2521
2555
  activeTurn.superseded = true;
2522
2556
  activeTurn.turnExecutionId = null;
2523
- void this.discardDraft(channelId);
2524
2557
  }
2525
2558
  invalidateChannelTurn(channelId) {
2526
2559
  const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
@@ -2683,17 +2716,22 @@ export class AgentsHost {
2683
2716
  });
2684
2717
  return null;
2685
2718
  }
2686
- const content = String(msg.content ?? msg.body ?? '').trim();
2687
- if (!content) {
2719
+ const incomingParts = buildHostedTurnContentParts({
2720
+ text: String(msg.content ?? msg.body ?? ''),
2721
+ attachments: msg.attachments,
2722
+ });
2723
+ if (incomingParts.length === 0) {
2688
2724
  this.logger.debug('ignored empty message', {
2689
2725
  channelId: msg.channel_id,
2690
2726
  messageId: msg.message_id,
2691
2727
  });
2692
2728
  return null;
2693
2729
  }
2730
+ const content = buildHostedIncomingContentText(incomingParts);
2694
2731
  return {
2695
2732
  authorId,
2696
2733
  content,
2734
+ incomingParts,
2697
2735
  };
2698
2736
  }
2699
2737
  // Only a task assigned to this agent drives a task-assignment turn: it keeps the message a