@wowyuarm/dsh-agent-team 0.1.2 → 0.1.3

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 (67) hide show
  1. package/README.md +1 -0
  2. package/README.zh.md +2 -0
  3. package/package.json +12 -3
  4. package/packages/agent-team/README.md +1 -1
  5. package/packages/agent-team/README.zh.md +1 -1
  6. package/packages/agent-team/lib/attachments.js +11 -12
  7. package/packages/agent-team/lib/auto-compaction.js +163 -0
  8. package/packages/agent-team/lib/index.js +214 -130
  9. package/packages/agent-team/lib/ledger.js +77 -70
  10. package/packages/agent-team/lib/recovery.js +30 -35
  11. package/packages/agent-team/lib/typert.host.js +33 -23
  12. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  13. package/packages/agent-team/lib/typert.remote-client.js +21 -21
  14. package/packages/agent-team/lib/types/attachments.d.ts +2 -2
  15. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  16. package/packages/agent-team/lib/types/auto-compaction.d.ts +38 -0
  17. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -0
  18. package/packages/agent-team/lib/types/index.d.ts +40 -12
  19. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  20. package/packages/agent-team/lib/types/ledger.d.ts +13 -0
  21. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/recovery.d.ts +15 -16
  23. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  24. package/packages/agent-team/preset/team-member/agent.cordis.yml +6 -1
  25. package/packages/client-agent-team/README.md +1 -1
  26. package/packages/client-agent-team/README.zh.md +1 -1
  27. package/packages/client-agent-team/lib/client.js +942 -534
  28. package/packages/client-agent-team/lib/client.js.map +1 -1
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +5 -166
  31. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +6 -20
  33. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -2
  35. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamComposer.js +0 -1
  37. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +34 -0
  38. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -0
  39. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +169 -0
  40. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +17 -0
  41. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -0
  42. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +120 -0
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.js +83 -43
  45. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +10 -23
  47. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/index.js +71 -1
  49. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +22 -0
  50. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +1 -0
  51. package/packages/client-agent-team/lib/types/client/member-session-input.js +98 -0
  52. package/packages/client-agent-team/lib/types/client/requests.d.ts +16 -0
  53. package/packages/client-agent-team/lib/types/client/requests.d.ts.map +1 -0
  54. package/packages/client-agent-team/lib/types/client/requests.js +22 -0
  55. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  56. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +6 -1
  57. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/task-refs.js +19 -0
  59. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +43 -3
  60. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/team-formatters.js +61 -4
  62. package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/team-membership.js +2 -1
  64. package/packages/tool-agent-team/lib/index.js +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts +0 -13
  66. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts.map +0 -1
  67. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.js +0 -90
@@ -51,14 +51,19 @@ import { scopeOf } from '@deepseek-ai/dsh-scope';
51
51
  import { SessionId } from '@deepseek-ai/dsh-session';
52
52
  import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
53
53
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
54
- import { ATTACHMENT_MAX_BYTES, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
54
+ import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
55
+ import { acceptedTaskCompactionMembers, AutoCompactionCoordinator } from "./auto-compaction.js";
55
56
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
56
- import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_ATTEMPTS } from "./recovery.js";
57
+ import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
57
58
  import { agentTeamDomainSpec } from "./spec.js";
58
59
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
59
60
  export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
60
61
  /** Process-stable marker carried by the final Team message tool definition. */
61
62
  export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
63
+ const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
64
+ const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
65
+ const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
66
+ const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
62
67
  function sameChangeScope(left, right) {
63
68
  if (left.kind === 'workspace' && right.kind === 'workspace')
64
69
  return left.workspaceId === right.workspaceId;
@@ -166,19 +171,26 @@ let AgentTeam = (() => {
166
171
  handles = new Map();
167
172
  /** Live selection refs let model edits take effect without disposing the Session. */
168
173
  modelSelections = new Map();
169
- diagnostics = new Map();
170
- runtimeErrors = new Map();
174
+ /**
175
+ * Why one Member shows error presence, per failure source. Reads prefer
176
+ * activation, then runtime, then compaction; slots clear independently, so
177
+ * a recovered runtime error re-reveals an outstanding compaction failure.
178
+ * Keyed by Member rather than Session so a restarted Session cannot leak
179
+ * stale keys.
180
+ */
181
+ memberFailures = new Map();
182
+ autoCompaction;
171
183
  /** Agent ids with a turn in flight; restarts must wait for the boundary. */
172
184
  runningAgents = new Set();
173
185
  notifiedInbox = new Map();
174
186
  attachmentGcTimer;
175
187
  recovery = new RecoveryCoordinator({
176
- inject: (memberId, attempt, kind) => {
177
- this.ctx.logger.info(`agent-team: automatic recovery steering member '${this.memberLabel(memberId)}' (attempt ${attempt}/${RECOVERY_MAX_ATTEMPTS}, ${kind})`);
178
- this.injectRecovery(memberId, attempt, kind);
188
+ wake: memberId => {
189
+ this.ctx.logger.info(`agent-team: automatic recovery wakeup for member '${this.memberLabel(memberId)}' after consecutive recoverable failures`);
190
+ this.injectRecovery(memberId);
179
191
  },
180
- onStandDown: (memberId, attempts) => {
181
- this.ctx.logger.warn(`agent-team: member '${this.memberLabel(memberId)}' still failing after ${attempts} automatic recovery attempts; leaving it in error for the operator`);
192
+ onStandDown: (memberId, consecutiveFailures) => {
193
+ this.ctx.logger.warn(`agent-team: member '${this.memberLabel(memberId)}' reached ${consecutiveFailures}/${RECOVERY_MAX_CONSECUTIVE_ERRORS} consecutive recoverable failures; leaving it in error for the operator`);
182
194
  },
183
195
  });
184
196
  lifecycleTail = Promise.resolve();
@@ -187,6 +199,21 @@ let AgentTeam = (() => {
187
199
  changeWaiters = new Set();
188
200
  constructor(ctx) {
189
201
  super(ctx, 'agentTeam');
202
+ this.autoCompaction = new AutoCompactionCoordinator({
203
+ agentForMember: memberId => this.handles.get(memberId)?.agent,
204
+ compactionForAgent: agent => this.ctx.agentPresets.serviceFor(agent, 'compaction'),
205
+ reactivate: memberId => this.reactivateMember(memberId),
206
+ failed: (memberId, _sessionId, diagnostic) => {
207
+ this.setMemberFailure(memberId, 'compaction', diagnostic);
208
+ this.emitAutoCompactionChanged(memberId);
209
+ },
210
+ cleared: memberId => {
211
+ if (!this.clearMemberFailure(memberId, 'compaction'))
212
+ return;
213
+ this.emitAutoCompactionChanged(memberId);
214
+ },
215
+ log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
216
+ });
190
217
  }
191
218
  /** Open the durable ledger and restore every enabled Member independently. */
192
219
  async [Service.init]() {
@@ -195,10 +222,10 @@ let AgentTeam = (() => {
195
222
  if (member === undefined)
196
223
  return;
197
224
  const message = error instanceof Error ? error.message : String(error);
198
- this.runtimeErrors.set(agent.id, message);
225
+ this.setMemberFailure(member.memberId, 'runtime', message);
199
226
  const kind = classifyRecoverableError(message);
200
227
  if (kind !== undefined)
201
- this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable error (${kind}); automatic recovery scheduled`);
228
+ this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
202
229
  this.recovery.onError(member.memberId, message);
203
230
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
204
231
  });
@@ -209,14 +236,14 @@ let AgentTeam = (() => {
209
236
  else
210
237
  this.runningAgents.delete(agent.id);
211
238
  if (status === 'running' && member !== undefined) {
212
- const recovered = this.runtimeErrors.delete(agent.id);
239
+ const recovered = this.clearMemberFailure(member.memberId, 'runtime');
213
240
  if (recovered)
214
241
  this.notifiedInbox.delete(member.memberId);
215
242
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
216
243
  this.notifyMember(agent);
217
244
  }
218
245
  // A turn that ends without an error closes any automatic recovery episode.
219
- if (status === 'idle' && member !== undefined && !this.runtimeErrors.has(agent.id)) {
246
+ if (status === 'idle' && member !== undefined && this.memberFailures.get(member.memberId)?.runtime === undefined) {
220
247
  this.recovery.onCleanTurnEnd(member.memberId);
221
248
  }
222
249
  });
@@ -224,6 +251,7 @@ let AgentTeam = (() => {
224
251
  this.ctx.effect(() => async () => {
225
252
  this.accepting = false;
226
253
  this.recovery.dispose();
254
+ await this.autoCompaction.dispose();
227
255
  if (this.attachmentGcTimer !== undefined)
228
256
  clearInterval(this.attachmentGcTimer);
229
257
  this.attachmentGcTimer = undefined;
@@ -264,7 +292,6 @@ let AgentTeam = (() => {
264
292
  members() {
265
293
  return this.requireLedger().listMembers().map(member => this.memberStatus(member));
266
294
  }
267
- /** Return only this Workspace's current Member projection to the Client. */
268
295
  /** Read-only navigation lookup for branded Task refs found in message bodies. */
269
296
  resolveTaskRefs(request) {
270
297
  this.requireWorkspace(request.workspaceId);
@@ -277,6 +304,7 @@ let AgentTeam = (() => {
277
304
  });
278
305
  return Object.freeze({ resolved: Object.freeze(this.requireLedger().resolveTaskRefs(request.workspaceId, taskRefs)) });
279
306
  }
307
+ /** Return only this Workspace's current Member projection to the Client. */
280
308
  membersForClient(request) {
281
309
  this.requireWorkspace(request.workspaceId);
282
310
  return this.members()
@@ -382,14 +410,7 @@ let AgentTeam = (() => {
382
410
  const result = await this.requireLedger().suspendMember({ ...request, actor: agentTeamHumanActor() });
383
411
  if (result.committed)
384
412
  this.emitCommitted(result.value.receipt);
385
- const handle = this.handles.get(request.memberId);
386
- if (handle !== undefined) {
387
- await handle.dispose();
388
- this.handles.delete(request.memberId);
389
- }
390
- this.modelSelections.delete(request.memberId);
391
- this.diagnostics.delete(request.memberId);
392
- this.clearMemberNotificationState(request.memberId);
413
+ await this.disposeMemberSession(request.memberId, result.value.member);
393
414
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
394
415
  });
395
416
  }
@@ -414,11 +435,17 @@ let AgentTeam = (() => {
414
435
  const member = this.requireLedger().getMember(request.memberId);
415
436
  if (member === undefined || member.workspaceId !== request.workspaceId)
416
437
  throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
417
- const storedError = this.storedErrorFor(member.memberId);
418
438
  this.recovery.stopTracking(request.memberId);
419
- const kind = classifyRecoverableError(storedError ?? '');
439
+ // An orphaned composition cannot be steered: its tools are gone, so a
440
+ // continuation prompt reaches an inert Member. Rebuild the Agent in place.
441
+ const handle = this.handles.get(request.memberId);
442
+ if (handle !== undefined && this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
443
+ this.ctx.logger.info(`agent-team: rebuilding member '${member.handle}' after its preset composition was orphaned by a reload`);
444
+ await this.reactivateMember(request.memberId);
445
+ return Object.freeze({ status: this.memberStatus(member) });
446
+ }
420
447
  this.ctx.logger.info(`agent-team: operator asked member '${member.handle}' to resume`);
421
- this.steerResume(member, this.resumeText(kind ?? 'unspecified failure', { operatorRequested: true }));
448
+ this.steerResume(member, this.manualResumeText());
422
449
  return Object.freeze({ status: this.memberStatus(member) });
423
450
  }
424
451
  /** Ledger handle for log lines; falls back to the raw id when unknown. */
@@ -439,26 +466,11 @@ let AgentTeam = (() => {
439
466
  this.attachmentGcTimer = setInterval(() => { void sweep(); }, 24 * 60 * 60 * 1000);
440
467
  this.attachmentGcTimer.unref?.();
441
468
  }
442
- /** Latest runtime error summary for a Member's live session, if any. */
443
- storedErrorFor(memberId) {
444
- const handle = this.handles.get(memberId);
445
- if (handle === undefined)
446
- return undefined;
447
- return this.runtimeErrors.get(handle.agent.id);
469
+ automaticResumeText() {
470
+ return 'Your previous turn ended early due to a temporary service error. Please continue the work you were doing before the error.';
448
471
  }
449
- /**
450
- * Continuation prompt per the recovery design: name the error family,
451
- * annotate automatic attempts, and on the final attempt ask for a graceful
452
- * handoff instead of another crash.
453
- */
454
- resumeText(kindLabel, options) {
455
- const opening = options?.operatorRequested === true ? 'The operator asked you to resume. ' : '';
456
- const attemptNote = options?.attempt === undefined ? '' : ` (automatic recovery, attempt ${options.attempt}/${RECOVERY_MAX_ATTEMPTS})`;
457
- let text = `${opening}Your previous turn ended early due to an error (${kindLabel})${attemptNote}. Please continue your work from where it stopped.`;
458
- if (options?.attempt === RECOVERY_MAX_ATTEMPTS) {
459
- text += ' If the same error recurs, stop and leave a short handoff note for the operator.';
460
- }
461
- return text;
472
+ manualResumeText() {
473
+ return 'The operator asked you to resume after the previous turn ended early. Please continue the work you were doing before the error.';
462
474
  }
463
475
  /**
464
476
  * Steer one continuation message into a Member's live session. Throws when
@@ -473,18 +485,22 @@ let AgentTeam = (() => {
473
485
  const body = notifications.length === 0 ? text : `${text}\n\n${this.notificationText(notifications, member.memberId)}`;
474
486
  const hint = createUserMessage({
475
487
  content: [{ type: 'text', text: body }],
476
- source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Recovery: continue your interrupted work.' },
488
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: RECOVERY_NOTICE_SUMMARY },
477
489
  });
490
+ for (const pending of [...handle.agent.inbox.nextStep, ...handle.agent.inbox.nextTurn]) {
491
+ if (this.isInboxNotice(pending))
492
+ handle.agent.inbox.remove(pending.id);
493
+ }
478
494
  handle.agent.steer(hint);
479
495
  }
480
- /** Automatic-recovery injection; throwing tells the coordinator the target is gone. */
481
- injectRecovery(memberId, attempt, kind) {
496
+ /** Automatic-recovery wakeup; throwing tells the coordinator the target is gone. */
497
+ injectRecovery(memberId) {
482
498
  const handle = this.handles.get(memberId);
483
499
  const agent = handle?.agent;
484
500
  const member = agent !== undefined ? this.memberForAgent(agent) : undefined;
485
501
  if (agent === undefined || member === undefined || member.state !== 'enabled')
486
502
  throw new Error(`member '${memberId}' cannot be recovered automatically`);
487
- this.steerResume(member, this.resumeText(kind, { attempt }));
503
+ this.steerResume(member, this.automaticResumeText());
488
504
  }
489
505
  /**
490
506
  * Human edit of one Member's mutable facts. A live model selection is
@@ -516,14 +532,7 @@ let AgentTeam = (() => {
516
532
  const result = await this.requireLedger().removeMember({ ...request, actor: agentTeamHumanActor() });
517
533
  if (result.committed)
518
534
  this.emitCommitted(result.value.receipt);
519
- const handle = this.handles.get(request.memberId);
520
- if (handle !== undefined) {
521
- await handle.dispose();
522
- this.handles.delete(request.memberId);
523
- }
524
- this.modelSelections.delete(request.memberId);
525
- this.diagnostics.delete(request.memberId);
526
- this.clearMemberNotificationState(request.memberId);
535
+ await this.disposeMemberSession(request.memberId, result.value.member);
527
536
  await this.cleanupRemovedMember(result.value.member);
528
537
  return result.value;
529
538
  });
@@ -538,37 +547,26 @@ let AgentTeam = (() => {
538
547
  }
539
548
  /** Human-only Channel membership grant; it never injects historical Thread bodies. */
540
549
  async joinChannel(request) {
541
- this.requireAccepting();
542
- this.requireWorkspace(request.workspaceId);
550
+ const actor = this.humanCall(request.workspaceId);
543
551
  const ledger = this.requireLedger();
544
552
  if (!ledger.hasCommitted(request.requestId))
545
553
  this.assertChannelMembersAvailable([request.memberId]);
546
- const result = await ledger.joinChannel({ ...request, actor: agentTeamHumanActor() });
554
+ const result = await ledger.joinChannel({ ...request, actor });
547
555
  if (result.committed)
548
556
  this.emitCommitted(result.value.receipt);
549
557
  return result.value;
550
558
  }
551
559
  /** Human-only Channel membership removal and Channel-scoped cleanup. */
552
560
  async removeChannelMember(request) {
553
- this.requireAccepting();
554
- this.requireWorkspace(request.workspaceId);
555
- const result = await this.requireLedger().removeChannelMember({ ...request, actor: agentTeamHumanActor() });
561
+ const actor = this.humanCall(request.workspaceId);
562
+ const result = await this.requireLedger().removeChannelMember({ ...request, actor });
556
563
  if (result.committed)
557
564
  this.emitCommitted(result.value.receipt);
558
565
  return result.value;
559
566
  }
560
567
  /** Human top-level Task creation; unfollowed Agent mentions use the two-send result. */
561
568
  async sendMessage(request) {
562
- this.requireAccepting();
563
- this.requireWorkspace(request.workspaceId);
564
- const metadata = await this.resolveMessageAttachments(request);
565
- const result = await this.requireLedger().sendMessage({
566
- ...request, body: this.appendAttachmentLines(request.body, metadata),
567
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
568
- actor: agentTeamHumanActor(),
569
- });
570
- this.emitCommittedOutcome(result);
571
- return result.value;
569
+ return this.sendMessageAs(this.humanCall(request.workspaceId), request);
572
570
  }
573
571
  /**
574
572
  * Resolve uploaded ids and agent-supplied absolute paths into one attachment
@@ -606,7 +604,7 @@ let AgentTeam = (() => {
606
604
  const trimmed = body.trim();
607
605
  if (metadata.length === 0)
608
606
  return trimmed;
609
- const lines = metadata.map(attachment => `[attachment] ${dshHomePath('agent-team', 'attachments', 'v1', attachment.attachmentId, attachment.name)}`);
607
+ const lines = metadata.map(attachment => `[attachment] ${attachmentPayloadPath(attachment.attachmentId, attachment.name)}`);
610
608
  return `${trimmed}\n${lines.join('\n')}`;
611
609
  }
612
610
  /** Upload one composer attachment into the cache; bytes are immutable once written. */
@@ -631,22 +629,12 @@ let AgentTeam = (() => {
631
629
  }
632
630
  /** Human existing-Thread reply; unread and revision conflicts are business outcomes. */
633
631
  async reply(request) {
634
- this.requireAccepting();
635
- this.requireWorkspace(request.workspaceId);
636
- const metadata = await this.resolveMessageAttachments(request);
637
- const result = await this.requireLedger().reply({
638
- ...request, body: this.appendAttachmentLines(request.body, metadata),
639
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
640
- actor: agentTeamHumanActor(),
641
- });
642
- this.emitCommittedOutcome(result);
643
- return result.value;
632
+ return this.replyAs(this.humanCall(request.workspaceId), request);
644
633
  }
645
634
  /** Human's personal Attention operation. */
646
635
  async changeAttention(request) {
647
- this.requireAccepting();
648
- this.requireWorkspace(request.workspaceId);
649
- const result = await this.requireLedger().changeAttention({ ...request, actor: agentTeamHumanActor() });
636
+ const actor = this.humanCall(request.workspaceId);
637
+ const result = await this.requireLedger().changeAttention({ ...request, actor });
650
638
  if (result.committed)
651
639
  this.emitCommitted(result.value.receipt);
652
640
  return result.value;
@@ -658,9 +646,8 @@ let AgentTeam = (() => {
658
646
  }
659
647
  /** Human's durable, atomically acknowledged Thread read. */
660
648
  async readThread(request) {
661
- this.requireAccepting();
662
- this.requireWorkspace(request.workspaceId);
663
- const result = await this.requireLedger().readThread({ ...request, actor: agentTeamHumanActor() });
649
+ const actor = this.humanCall(request.workspaceId);
650
+ const result = await this.requireLedger().readThread({ ...request, actor });
664
651
  if (result.committed)
665
652
  this.emitCommitted(result.value.receipt);
666
653
  return result.value;
@@ -682,37 +669,15 @@ let AgentTeam = (() => {
682
669
  }
683
670
  /** Agent-only top-level Task creation. Workspace identity is verified against the live binding. */
684
671
  async sendMessageForAgent(agent, request) {
685
- this.requireAccepting();
686
- const actor = this.memberActor(agent);
687
- this.requireAgentWorkspace(actor, request.workspaceId);
688
- const metadata = await this.resolveMessageAttachments(request);
689
- const result = await this.requireLedger().sendMessage({
690
- ...request, body: this.appendAttachmentLines(request.body, metadata),
691
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
692
- actor,
693
- });
694
- this.emitCommittedOutcome(result);
695
- return result.value;
672
+ return this.sendMessageAs(this.memberCall(agent, request.workspaceId), request);
696
673
  }
697
674
  /** Agent-only existing-Thread reply. */
698
675
  async replyForAgent(agent, request) {
699
- this.requireAccepting();
700
- const actor = this.memberActor(agent);
701
- this.requireAgentWorkspace(actor, request.workspaceId);
702
- const metadata = await this.resolveMessageAttachments(request);
703
- const result = await this.requireLedger().reply({
704
- ...request, body: this.appendAttachmentLines(request.body, metadata),
705
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
706
- actor,
707
- });
708
- this.emitCommittedOutcome(result);
709
- return result.value;
676
+ return this.replyAs(this.memberCall(agent, request.workspaceId), request);
710
677
  }
711
678
  /** Agent-only personal Attention change. */
712
679
  async changeAttentionForAgent(agent, request) {
713
- this.requireAccepting();
714
- const actor = this.memberActor(agent);
715
- this.requireAgentWorkspace(actor, request.workspaceId);
680
+ const actor = this.memberCall(agent, request.workspaceId);
716
681
  const result = await this.requireLedger().changeAttention({ ...request, actor });
717
682
  if (result.committed)
718
683
  this.emitCommitted(result.value.receipt);
@@ -725,9 +690,7 @@ let AgentTeam = (() => {
725
690
  }
726
691
  /** Agent-only Claim mutation. */
727
692
  async changeClaimForAgent(agent, request) {
728
- this.requireAccepting();
729
- const actor = this.memberActor(agent);
730
- this.requireAgentWorkspace(actor, request.workspaceId);
693
+ const actor = this.memberCall(agent, request.workspaceId);
731
694
  const result = await this.requireLedger().changeClaim({ ...request, actor });
732
695
  this.emitCommittedOutcome(result);
733
696
  return result.value;
@@ -743,9 +706,7 @@ let AgentTeam = (() => {
743
706
  return this.requireLedger().inbox(actor, request);
744
707
  }
745
708
  async readThreadForAgent(agent, request) {
746
- this.requireAccepting();
747
- const actor = this.memberActor(agent);
748
- this.requireAgentWorkspace(actor, request.workspaceId);
709
+ const actor = this.memberCall(agent, request.workspaceId);
749
710
  const result = await this.requireLedger().readThread({ ...request, actor });
750
711
  if (result.committed)
751
712
  this.emitCommitted(result.value.receipt);
@@ -782,6 +743,41 @@ let AgentTeam = (() => {
782
743
  throw new Error(`Agent Member '${memberId}' is not available for Channel membership`);
783
744
  }
784
745
  }
746
+ /** Shared Task-creation commit: resolve uploads into metadata lines and append through the ledger. */
747
+ async sendMessageAs(actor, request) {
748
+ const metadata = await this.resolveMessageAttachments(request);
749
+ const result = await this.requireLedger().sendMessage({
750
+ ...request, body: this.appendAttachmentLines(request.body, metadata),
751
+ ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
752
+ actor,
753
+ });
754
+ this.emitCommittedOutcome(result);
755
+ return result.value;
756
+ }
757
+ /** Shared existing-Thread reply commit: same upload resolution and outcome emission. */
758
+ async replyAs(actor, request) {
759
+ const metadata = await this.resolveMessageAttachments(request);
760
+ const result = await this.requireLedger().reply({
761
+ ...request, body: this.appendAttachmentLines(request.body, metadata),
762
+ ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
763
+ actor,
764
+ });
765
+ this.emitCommittedOutcome(result);
766
+ return result.value;
767
+ }
768
+ /** Fence one Human Remote call: accepting Host, known Workspace, Human actor. */
769
+ humanCall(workspaceId) {
770
+ this.requireAccepting();
771
+ this.requireWorkspace(workspaceId);
772
+ return agentTeamHumanActor();
773
+ }
774
+ /** Fence one Member call: accepting Host, live Member binding, matching Workspace. */
775
+ memberCall(agent, workspaceId) {
776
+ this.requireAccepting();
777
+ const actor = this.memberActor(agent);
778
+ this.requireAgentWorkspace(actor, workspaceId);
779
+ return actor;
780
+ }
785
781
  memberActor(agent) {
786
782
  const member = this.memberForAgent(agent);
787
783
  if (member === undefined)
@@ -853,20 +849,50 @@ let AgentTeam = (() => {
853
849
  await workspace.attachSession(member.sessionId);
854
850
  this.handles.set(member.memberId, created);
855
851
  this.modelSelections.set(member.memberId, selected);
856
- this.diagnostics.delete(member.memberId);
852
+ this.clearMemberFailure(member.memberId, 'activation');
857
853
  this.nameMemberSession(member, created.agent);
858
854
  this.notifyMember(created.agent);
855
+ this.autoCompaction.activated(member.memberId);
859
856
  }
860
857
  catch (error) {
861
858
  await created?.dispose();
862
859
  this.modelSelections.delete(member.memberId);
863
- this.diagnostics.set(member.memberId, error instanceof Error ? error.message : String(error));
860
+ this.setMemberFailure(member.memberId, 'activation', error instanceof Error ? error.message : String(error));
864
861
  }
865
862
  finally {
866
863
  // Activation only changes this Workspace's presence projection.
867
864
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
868
865
  }
869
866
  }
867
+ /**
868
+ * Rebuild one enabled Member in place from its persisted Session.
869
+ *
870
+ * A bundle-row reload tears down the preset roster subtree, which prunes the
871
+ * standing mount while live agents keep their dead scope bindings: the
872
+ * Member keeps its session but loses its composed tools and services.
873
+ * Re-running the preset composition requires a fresh Agent, and disposal is
874
+ * the cost — the Web Client marks the recreated Session unavailable until it
875
+ * is reopened, the same trade the shipped suspend/resume cycle makes.
876
+ */
877
+ reactivateMember(memberId) {
878
+ return this.enqueueLifecycle(async () => {
879
+ const member = this.requireLedger().getMember(memberId);
880
+ if (member === undefined || member.state !== 'enabled')
881
+ return false;
882
+ const stale = this.handles.get(memberId);
883
+ if (stale !== undefined) {
884
+ this.handles.delete(memberId);
885
+ this.modelSelections.delete(memberId);
886
+ // The composition-loss diagnostic this heal answers is stale once the
887
+ // rebuild starts; a later activation must not resurface it.
888
+ this.clearMemberFailure(memberId, 'compaction');
889
+ this.emitAutoCompactionChanged(memberId);
890
+ await stale.dispose();
891
+ }
892
+ await this.activateMember(member);
893
+ return this.handles.has(memberId);
894
+ });
895
+ }
870
896
  /**
871
897
  * Default an untitled Member Session to its handle so the ordinary Session
872
898
  * list names it. An explicit rename or any earlier title always wins; the
@@ -921,17 +947,35 @@ let AgentTeam = (() => {
921
947
  return Object.freeze({ member, availability: 'inactive', presence: 'unavailable' });
922
948
  if (member.state === 'suspended')
923
949
  return Object.freeze({ member, availability: 'suspended', presence: 'unavailable' });
924
- const diagnostic = this.diagnostics.get(member.memberId);
925
- if (diagnostic !== undefined)
926
- return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic });
950
+ const failures = this.memberFailures.get(member.memberId);
951
+ if (failures?.activation !== undefined)
952
+ return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable', diagnostic: failures.activation });
927
953
  const handle = this.handles.get(member.memberId);
928
954
  if (handle === undefined)
929
955
  return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable' });
930
- const runtimeError = this.runtimeErrors.get(handle.agent.id);
956
+ if (this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
957
+ return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: ORPHANED_MEMBER_DIAGNOSTIC });
958
+ }
959
+ const runtimeError = failures?.runtime ?? failures?.compaction;
931
960
  if (runtimeError !== undefined)
932
961
  return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
933
962
  return Object.freeze({ member, availability: 'active', presence: handle.agent.status === 'running' ? 'working' : 'available' });
934
963
  }
964
+ setMemberFailure(memberId, slot, message) {
965
+ const failures = this.memberFailures.get(memberId) ?? {};
966
+ failures[slot] = message;
967
+ this.memberFailures.set(memberId, failures);
968
+ }
969
+ clearMemberFailure(memberId, slot) {
970
+ const failures = this.memberFailures.get(memberId);
971
+ if (failures === undefined || failures[slot] === undefined)
972
+ return false;
973
+ if (Object.keys(failures).length === 1)
974
+ this.memberFailures.delete(memberId);
975
+ else
976
+ delete failures[slot];
977
+ return true;
978
+ }
935
979
  requireWorkspace(workspaceId) {
936
980
  const workspace = this.ctx.workspaceRegistry.get(workspaceId);
937
981
  if (workspace === undefined)
@@ -961,6 +1005,13 @@ let AgentTeam = (() => {
961
1005
  if (handle !== undefined)
962
1006
  this.notifyMember(handle.agent);
963
1007
  }
1008
+ const compactionMembers = acceptedTaskCompactionMembers(operation);
1009
+ if (compactionMembers !== undefined)
1010
+ this.autoCompaction.schedule(compactionMembers);
1011
+ }
1012
+ emitAutoCompactionChanged(memberId) {
1013
+ const workspaceId = this.ledger?.getMember(memberId)?.workspaceId;
1014
+ this.emitChanged(workspaceId === undefined ? undefined : [{ kind: 'workspace', workspaceId }]);
964
1015
  }
965
1016
  /** Wake from durable unread state with bounded facts for direct and state-changing work. */
966
1017
  notifyMember(agent) {
@@ -977,12 +1028,19 @@ let AgentTeam = (() => {
977
1028
  ]));
978
1029
  if (this.notifiedInbox.get(member.memberId) === signature)
979
1030
  return;
980
- const existingHint = [...agent.inbox.nextStep, ...agent.inbox.nextTurn].find(message => message.source.kind === 'plugin' && message.source.plugin === '@wowyuarm/dsh-agent-team');
981
- if (existingHint !== undefined)
982
- agent.inbox.remove(existingHint.id);
1031
+ const pending = [...agent.inbox.nextStep, ...agent.inbox.nextTurn];
1032
+ // steerResume already combines the recovery instruction and these durable
1033
+ // facts. Its synchronous running transition must not replace that notice.
1034
+ if (pending.some(message => this.isRecoveryNotice(message))) {
1035
+ this.notifiedInbox.set(member.memberId, signature);
1036
+ return;
1037
+ }
1038
+ const existingInboxHint = pending.find(message => this.isInboxNotice(message));
1039
+ if (existingInboxHint !== undefined)
1040
+ agent.inbox.remove(existingInboxHint.id);
983
1041
  const hint = createUserMessage({
984
1042
  content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
985
- source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Team Inbox has unread work.' },
1043
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: INBOX_NOTICE_SUMMARY },
986
1044
  });
987
1045
  this.notifiedInbox.set(member.memberId, signature);
988
1046
  try {
@@ -993,6 +1051,16 @@ let AgentTeam = (() => {
993
1051
  throw error;
994
1052
  }
995
1053
  }
1054
+ isInboxNotice(message) {
1055
+ const source = message.source;
1056
+ return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID
1057
+ && source.form === 'notice' && source.summary === INBOX_NOTICE_SUMMARY;
1058
+ }
1059
+ isRecoveryNotice(message) {
1060
+ const source = message.source;
1061
+ return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID
1062
+ && source.form === 'notice' && source.summary === RECOVERY_NOTICE_SUMMARY;
1063
+ }
996
1064
  notificationText(notifications, readerId) {
997
1065
  const maxCharacters = 32 * 1024;
998
1066
  const sections = ['Team Inbox has unread work.'];
@@ -1061,6 +1129,22 @@ let AgentTeam = (() => {
1061
1129
  ? ` Released Claims: ${activity.releasedClaimRefs.join(', ')}.` : '';
1062
1130
  return `Team Task update\n${actor} ${activity.kind} Task ${activity.taskRef}.${released}`;
1063
1131
  }
1132
+ /** Dispose one live Member Session and drop its per-Member runtime state. */
1133
+ async disposeMemberSession(memberId, member) {
1134
+ this.clearMemberRecoveryState(member);
1135
+ const handle = this.handles.get(memberId);
1136
+ if (handle !== undefined) {
1137
+ await handle.dispose();
1138
+ this.handles.delete(memberId);
1139
+ }
1140
+ this.modelSelections.delete(memberId);
1141
+ this.clearMemberFailure(memberId, 'activation');
1142
+ }
1143
+ clearMemberRecoveryState(member) {
1144
+ this.recovery.stopTracking(member.memberId);
1145
+ this.clearMemberFailure(member.memberId, 'runtime');
1146
+ this.clearMemberNotificationState(member.memberId);
1147
+ }
1064
1148
  clearMemberNotificationState(memberId) {
1065
1149
  this.notifiedInbox.delete(memberId);
1066
1150
  }