@wowyuarm/dsh-agent-team 0.1.1 → 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 (92) hide show
  1. package/README.md +1 -0
  2. package/README.zh.md +2 -0
  3. package/package.json +42 -25
  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 +262 -189
  9. package/packages/agent-team/lib/ledger.js +140 -109
  10. package/packages/agent-team/lib/recovery.js +30 -35
  11. package/packages/agent-team/lib/spec.js +10 -4
  12. package/packages/agent-team/lib/typert.host.js +68 -71
  13. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -3
  14. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  15. package/packages/agent-team/lib/typert.remote-client.js +41 -56
  16. package/packages/agent-team/lib/types/attachments.d.ts +2 -2
  17. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/auto-compaction.d.ts +38 -0
  19. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -0
  20. package/packages/agent-team/lib/types/index.d.ts +49 -24
  21. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/ledger.d.ts +24 -11
  23. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  24. package/packages/agent-team/lib/types/recovery.d.ts +15 -16
  25. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/types.d.ts +20 -12
  28. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  29. package/packages/agent-team/preset/team-member/agent.cordis.yml +8 -3
  30. package/packages/client-agent-team/README.md +3 -2
  31. package/packages/client-agent-team/README.zh.md +3 -2
  32. package/packages/client-agent-team/lib/client.js +1507 -525
  33. package/packages/client-agent-team/lib/client.js.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  35. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -182
  37. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  38. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +27 -5
  40. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +19 -4
  42. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  43. package/packages/client-agent-team/lib/types/client/TeamComposer.js +22 -2
  44. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  47. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +34 -0
  48. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -0
  49. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +169 -0
  50. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts +17 -0
  51. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +120 -0
  53. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -2
  54. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamMessage.js +170 -18
  56. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +4 -0
  57. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +70 -13
  59. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  61. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  62. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/index.js +72 -2
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts +8 -2
  65. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/locales.js +8 -2
  67. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +22 -0
  68. package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/member-session-input.js +98 -0
  70. package/packages/client-agent-team/lib/types/client/requests.d.ts +16 -0
  71. package/packages/client-agent-team/lib/types/client/requests.d.ts.map +1 -0
  72. package/packages/client-agent-team/lib/types/client/requests.js +22 -0
  73. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
  74. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
  76. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
  77. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
  78. package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
  79. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -2
  80. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +25 -0
  82. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
  83. package/packages/client-agent-team/lib/types/client/task-refs.js +77 -0
  84. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +54 -1
  85. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  86. package/packages/client-agent-team/lib/types/client/team-formatters.js +83 -2
  87. package/packages/client-agent-team/lib/types/client/team-membership.d.ts.map +1 -1
  88. package/packages/client-agent-team/lib/types/client/team-membership.js +2 -1
  89. package/packages/tool-agent-team/lib/index.js +4 -4
  90. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts +0 -13
  91. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.d.ts.map +0 -1
  92. package/packages/client-agent-team/lib/types/client/TeamInboxPanel.js +0 -90
@@ -44,20 +44,26 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
44
44
  import { join } from 'node:path';
45
45
  import { isDeepStrictEqual } from 'node:util';
46
46
  import { Service } from '@deepseek-ai/cordis';
47
+ import { installModelSelection } from '@deepseek-ai/dsh-agent';
47
48
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
48
49
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
49
50
  import { scopeOf } from '@deepseek-ai/dsh-scope';
50
51
  import { SessionId } from '@deepseek-ai/dsh-session';
51
52
  import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
52
53
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
53
- 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";
54
56
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
55
- import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_ATTEMPTS } from "./recovery.js";
57
+ import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
56
58
  import { agentTeamDomainSpec } from "./spec.js";
57
59
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
58
60
  export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
59
61
  /** Process-stable marker carried by the final Team message tool definition. */
60
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.';
61
67
  function sameChangeScope(left, right) {
62
68
  if (left.kind === 'workspace' && right.kind === 'workspace')
63
69
  return left.workspaceId === right.workspaceId;
@@ -83,13 +89,13 @@ export const AGENT_TEAM_TOOL_NAMES = Object.freeze([
83
89
  let AgentTeam = (() => {
84
90
  let _classSuper = TypertRemoteService;
85
91
  let _instanceExtraInitializers = [];
92
+ let _resolveTaskRefs_decorators;
86
93
  let _membersForClient_decorators;
87
94
  let _changes_decorators;
88
95
  let _createChannel_decorators;
89
96
  let _updateChannel_decorators;
90
97
  let _addMember_decorators;
91
98
  let _recoverMember_decorators;
92
- let _restartMember_decorators;
93
99
  let _updateMember_decorators;
94
100
  let _changeTask_decorators;
95
101
  let _joinChannel_decorators;
@@ -107,13 +113,13 @@ let AgentTeam = (() => {
107
113
  return class AgentTeam extends _classSuper {
108
114
  static {
109
115
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
116
+ _resolveTaskRefs_decorators = [Remote('resolveTaskRefs')];
110
117
  _membersForClient_decorators = [Remote('members')];
111
118
  _changes_decorators = [Remote('changes')];
112
119
  _createChannel_decorators = [Remote('createChannel')];
113
120
  _updateChannel_decorators = [Remote('updateChannel')];
114
121
  _addMember_decorators = [Remote('addMember')];
115
122
  _recoverMember_decorators = [Remote('recoverMember')];
116
- _restartMember_decorators = [Remote('restartMember')];
117
123
  _updateMember_decorators = [Remote('updateMember')];
118
124
  _changeTask_decorators = [Remote('changeTask')];
119
125
  _joinChannel_decorators = [Remote('joinChannel')];
@@ -128,13 +134,13 @@ let AgentTeam = (() => {
128
134
  _threadObservations_decorators = [Remote('threadObservations')];
129
135
  _threadHistory_decorators = [Remote('threadHistory')];
130
136
  _view_decorators = [Remote('view')];
137
+ __esDecorate(this, null, _resolveTaskRefs_decorators, { kind: "method", name: "resolveTaskRefs", static: false, private: false, access: { has: obj => "resolveTaskRefs" in obj, get: obj => obj.resolveTaskRefs }, metadata: _metadata }, null, _instanceExtraInitializers);
131
138
  __esDecorate(this, null, _membersForClient_decorators, { kind: "method", name: "membersForClient", static: false, private: false, access: { has: obj => "membersForClient" in obj, get: obj => obj.membersForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
132
139
  __esDecorate(this, null, _changes_decorators, { kind: "method", name: "changes", static: false, private: false, access: { has: obj => "changes" in obj, get: obj => obj.changes }, metadata: _metadata }, null, _instanceExtraInitializers);
133
140
  __esDecorate(this, null, _createChannel_decorators, { kind: "method", name: "createChannel", static: false, private: false, access: { has: obj => "createChannel" in obj, get: obj => obj.createChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
134
141
  __esDecorate(this, null, _updateChannel_decorators, { kind: "method", name: "updateChannel", static: false, private: false, access: { has: obj => "updateChannel" in obj, get: obj => obj.updateChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
135
142
  __esDecorate(this, null, _addMember_decorators, { kind: "method", name: "addMember", static: false, private: false, access: { has: obj => "addMember" in obj, get: obj => obj.addMember }, metadata: _metadata }, null, _instanceExtraInitializers);
136
143
  __esDecorate(this, null, _recoverMember_decorators, { kind: "method", name: "recoverMember", static: false, private: false, access: { has: obj => "recoverMember" in obj, get: obj => obj.recoverMember }, metadata: _metadata }, null, _instanceExtraInitializers);
137
- __esDecorate(this, null, _restartMember_decorators, { kind: "method", name: "restartMember", static: false, private: false, access: { has: obj => "restartMember" in obj, get: obj => obj.restartMember }, metadata: _metadata }, null, _instanceExtraInitializers);
138
144
  __esDecorate(this, null, _updateMember_decorators, { kind: "method", name: "updateMember", static: false, private: false, access: { has: obj => "updateMember" in obj, get: obj => obj.updateMember }, metadata: _metadata }, null, _instanceExtraInitializers);
139
145
  __esDecorate(this, null, _changeTask_decorators, { kind: "method", name: "changeTask", static: false, private: false, access: { has: obj => "changeTask" in obj, get: obj => obj.changeTask }, metadata: _metadata }, null, _instanceExtraInitializers);
140
146
  __esDecorate(this, null, _joinChannel_decorators, { kind: "method", name: "joinChannel", static: false, private: false, access: { has: obj => "joinChannel" in obj, get: obj => obj.joinChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -163,19 +169,28 @@ let AgentTeam = (() => {
163
169
  domain = __runInitializers(this, _instanceExtraInitializers);
164
170
  ledger;
165
171
  handles = new Map();
166
- diagnostics = new Map();
167
- runtimeErrors = new Map();
172
+ /** Live selection refs let model edits take effect without disposing the Session. */
173
+ modelSelections = 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;
168
183
  /** Agent ids with a turn in flight; restarts must wait for the boundary. */
169
184
  runningAgents = new Set();
170
185
  notifiedInbox = new Map();
171
186
  attachmentGcTimer;
172
187
  recovery = new RecoveryCoordinator({
173
- inject: (memberId, attempt, kind) => {
174
- this.ctx.logger.info(`agent-team: automatic recovery steering member '${this.memberLabel(memberId)}' (attempt ${attempt}/${RECOVERY_MAX_ATTEMPTS}, ${kind})`);
175
- 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);
176
191
  },
177
- onStandDown: (memberId, attempts) => {
178
- 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`);
179
194
  },
180
195
  });
181
196
  lifecycleTail = Promise.resolve();
@@ -184,6 +199,21 @@ let AgentTeam = (() => {
184
199
  changeWaiters = new Set();
185
200
  constructor(ctx) {
186
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
+ });
187
217
  }
188
218
  /** Open the durable ledger and restore every enabled Member independently. */
189
219
  async [Service.init]() {
@@ -192,10 +222,10 @@ let AgentTeam = (() => {
192
222
  if (member === undefined)
193
223
  return;
194
224
  const message = error instanceof Error ? error.message : String(error);
195
- this.runtimeErrors.set(agent.id, message);
225
+ this.setMemberFailure(member.memberId, 'runtime', message);
196
226
  const kind = classifyRecoverableError(message);
197
227
  if (kind !== undefined)
198
- 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`);
199
229
  this.recovery.onError(member.memberId, message);
200
230
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
201
231
  });
@@ -206,14 +236,14 @@ let AgentTeam = (() => {
206
236
  else
207
237
  this.runningAgents.delete(agent.id);
208
238
  if (status === 'running' && member !== undefined) {
209
- const recovered = this.runtimeErrors.delete(agent.id);
239
+ const recovered = this.clearMemberFailure(member.memberId, 'runtime');
210
240
  if (recovered)
211
241
  this.notifiedInbox.delete(member.memberId);
212
242
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
213
243
  this.notifyMember(agent);
214
244
  }
215
245
  // A turn that ends without an error closes any automatic recovery episode.
216
- if (status === 'idle' && member !== undefined && !this.runtimeErrors.has(agent.id)) {
246
+ if (status === 'idle' && member !== undefined && this.memberFailures.get(member.memberId)?.runtime === undefined) {
217
247
  this.recovery.onCleanTurnEnd(member.memberId);
218
248
  }
219
249
  });
@@ -221,6 +251,7 @@ let AgentTeam = (() => {
221
251
  this.ctx.effect(() => async () => {
222
252
  this.accepting = false;
223
253
  this.recovery.dispose();
254
+ await this.autoCompaction.dispose();
224
255
  if (this.attachmentGcTimer !== undefined)
225
256
  clearInterval(this.attachmentGcTimer);
226
257
  this.attachmentGcTimer = undefined;
@@ -228,6 +259,7 @@ let AgentTeam = (() => {
228
259
  await this.lifecycleTail;
229
260
  await Promise.all([...this.handles.values()].map(handle => handle.dispose()));
230
261
  this.handles.clear();
262
+ this.modelSelections.clear();
231
263
  this.runningAgents.clear();
232
264
  await domain.close();
233
265
  }, 'agentTeam.dispose');
@@ -260,6 +292,18 @@ let AgentTeam = (() => {
260
292
  members() {
261
293
  return this.requireLedger().listMembers().map(member => this.memberStatus(member));
262
294
  }
295
+ /** Read-only navigation lookup for branded Task refs found in message bodies. */
296
+ resolveTaskRefs(request) {
297
+ this.requireWorkspace(request.workspaceId);
298
+ const seen = new Set();
299
+ const taskRefs = request.taskRefs.filter(taskRef => {
300
+ if (seen.has(taskRef))
301
+ return false;
302
+ seen.add(taskRef);
303
+ return true;
304
+ });
305
+ return Object.freeze({ resolved: Object.freeze(this.requireLedger().resolveTaskRefs(request.workspaceId, taskRefs)) });
306
+ }
263
307
  /** Return only this Workspace's current Member projection to the Client. */
264
308
  membersForClient(request) {
265
309
  this.requireWorkspace(request.workspaceId);
@@ -366,13 +410,7 @@ let AgentTeam = (() => {
366
410
  const result = await this.requireLedger().suspendMember({ ...request, actor: agentTeamHumanActor() });
367
411
  if (result.committed)
368
412
  this.emitCommitted(result.value.receipt);
369
- const handle = this.handles.get(request.memberId);
370
- if (handle !== undefined) {
371
- await handle.dispose();
372
- this.handles.delete(request.memberId);
373
- }
374
- this.diagnostics.delete(request.memberId);
375
- this.clearMemberNotificationState(request.memberId);
413
+ await this.disposeMemberSession(request.memberId, result.value.member);
376
414
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
377
415
  });
378
416
  }
@@ -397,54 +435,19 @@ let AgentTeam = (() => {
397
435
  const member = this.requireLedger().getMember(request.memberId);
398
436
  if (member === undefined || member.workspaceId !== request.workspaceId)
399
437
  throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
400
- const storedError = this.storedErrorFor(member.memberId);
401
438
  this.recovery.stopTracking(request.memberId);
402
- 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
+ }
403
447
  this.ctx.logger.info(`agent-team: operator asked member '${member.handle}' to resume`);
404
- this.steerResume(member, this.resumeText(kind ?? 'unspecified failure', { operatorRequested: true }));
448
+ this.steerResume(member, this.manualResumeText());
405
449
  return Object.freeze({ status: this.memberStatus(member) });
406
450
  }
407
- /**
408
- * Restart one enabled Member's live session in place: dispose the handle and
409
- * reactivate on the same session id, so the transcript, private memory, and
410
- * every ledger fact survive while tools and prompt templates are re-minted
411
- * from the running plugin. The audit operation changes no projection state.
412
- */
413
- async restartMember(request) {
414
- return this.enqueueLifecycle(async () => {
415
- this.requireAccepting();
416
- this.requireWorkspace(request.workspaceId);
417
- const stored = this.requireLedger().getMember(request.memberId);
418
- if (stored === undefined || stored.workspaceId !== request.workspaceId)
419
- throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
420
- if (stored.state !== 'enabled')
421
- throw new Error(`Agent Member '${stored.handle}' is ${stored.state}; only enabled Members can be restarted`);
422
- const active = this.handles.get(request.memberId);
423
- if (active === undefined)
424
- throw new Error(`Agent Member '${stored.handle}' has no active session to restart`);
425
- if (this.runningAgents.has(active.agent.id))
426
- throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before restarting`);
427
- const result = await this.requireLedger().restartMember({ ...request, actor: agentTeamHumanActor() });
428
- if (result.committed)
429
- this.emitCommitted(result.value.receipt);
430
- // Drop the old handle's transient state: pending recovery episodes and
431
- // error markers belong to the disposed agent, not to the Member.
432
- this.recovery.stopTracking(request.memberId);
433
- this.runtimeErrors.delete(active.agent.id);
434
- this.notifiedInbox.delete(request.memberId);
435
- await active.dispose();
436
- this.handles.delete(request.memberId);
437
- this.diagnostics.delete(request.memberId);
438
- await this.activateMember(stored);
439
- const reactivated = this.handles.get(request.memberId);
440
- if (reactivated === undefined) {
441
- // Reactivation failed; the activation diagnostic carries the reason and
442
- // the audit record stays honest about the attempt.
443
- throw new Error(`Agent Member '${stored.handle}' failed to reactivate: ${this.diagnostics.get(request.memberId) ?? 'unknown error'}`);
444
- }
445
- return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
446
- });
447
- }
448
451
  /** Ledger handle for log lines; falls back to the raw id when unknown. */
449
452
  memberLabel(memberId) {
450
453
  return this.ledger?.getMember(memberId)?.handle ?? memberId;
@@ -463,26 +466,11 @@ let AgentTeam = (() => {
463
466
  this.attachmentGcTimer = setInterval(() => { void sweep(); }, 24 * 60 * 60 * 1000);
464
467
  this.attachmentGcTimer.unref?.();
465
468
  }
466
- /** Latest runtime error summary for a Member's live session, if any. */
467
- storedErrorFor(memberId) {
468
- const handle = this.handles.get(memberId);
469
- if (handle === undefined)
470
- return undefined;
471
- 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.';
472
471
  }
473
- /**
474
- * Continuation prompt per the recovery design: name the error family,
475
- * annotate automatic attempts, and on the final attempt ask for a graceful
476
- * handoff instead of another crash.
477
- */
478
- resumeText(kindLabel, options) {
479
- const opening = options?.operatorRequested === true ? 'The operator asked you to resume. ' : '';
480
- const attemptNote = options?.attempt === undefined ? '' : ` (automatic recovery, attempt ${options.attempt}/${RECOVERY_MAX_ATTEMPTS})`;
481
- let text = `${opening}Your previous turn ended early due to an error (${kindLabel})${attemptNote}. Please continue your work from where it stopped.`;
482
- if (options?.attempt === RECOVERY_MAX_ATTEMPTS) {
483
- text += ' If the same error recurs, stop and leave a short handoff note for the operator.';
484
- }
485
- 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.';
486
474
  }
487
475
  /**
488
476
  * Steer one continuation message into a Member's live session. Throws when
@@ -494,27 +482,31 @@ let AgentTeam = (() => {
494
482
  if (handle === undefined)
495
483
  throw new Error(`member '${member.handle}' has no active session`);
496
484
  const notifications = this.requireLedger().notificationFacts(member.memberId, { workspaceId: member.workspaceId });
497
- const body = notifications.length === 0 ? text : `${text}\n\n${this.notificationText(notifications)}`;
485
+ const body = notifications.length === 0 ? text : `${text}\n\n${this.notificationText(notifications, member.memberId)}`;
498
486
  const hint = createUserMessage({
499
487
  content: [{ type: 'text', text: body }],
500
- 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 },
501
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
+ }
502
494
  handle.agent.steer(hint);
503
495
  }
504
- /** Automatic-recovery injection; throwing tells the coordinator the target is gone. */
505
- injectRecovery(memberId, attempt, kind) {
496
+ /** Automatic-recovery wakeup; throwing tells the coordinator the target is gone. */
497
+ injectRecovery(memberId) {
506
498
  const handle = this.handles.get(memberId);
507
499
  const agent = handle?.agent;
508
500
  const member = agent !== undefined ? this.memberForAgent(agent) : undefined;
509
501
  if (agent === undefined || member === undefined || member.state !== 'enabled')
510
502
  throw new Error(`member '${memberId}' cannot be recovered automatically`);
511
- this.steerResume(member, this.resumeText(kind, { attempt }));
503
+ this.steerResume(member, this.automaticResumeText());
512
504
  }
513
505
  /**
514
- * Human edit of one Member's mutable facts. A model change restarts an
515
- * active Member: single-Session Members read their selection only at
516
- * activation, so the quiescing dispose plus reactivation applies the new
517
- * provider/model immediately instead of at the next Host restart.
506
+ * Human edit of one Member's mutable facts. A live model selection is
507
+ * updated in place: disposing an Agent emits session/disposed, which makes
508
+ * the Web Client permanently mark the same Session id unavailable even when
509
+ * Team immediately recreates it.
518
510
  */
519
511
  async updateMember(request) {
520
512
  return this.enqueueLifecycle(async () => {
@@ -526,9 +518,10 @@ let AgentTeam = (() => {
526
518
  const stored = result.value.member;
527
519
  const active = this.handles.get(request.memberId);
528
520
  if (active !== undefined && !isDeepStrictEqual(previous?.model ?? undefined, stored.model ?? undefined)) {
529
- await active.dispose();
530
- this.handles.delete(request.memberId);
531
- await this.activateMember(stored);
521
+ const selection = this.modelSelections.get(request.memberId);
522
+ if (selection === undefined)
523
+ throw new Error(`Agent Member '${stored.handle}' has no live model selection`);
524
+ selection.current = stored.model ?? this.ctx.agentDefaultModel.currentSelection();
532
525
  }
533
526
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
534
527
  });
@@ -539,13 +532,7 @@ let AgentTeam = (() => {
539
532
  const result = await this.requireLedger().removeMember({ ...request, actor: agentTeamHumanActor() });
540
533
  if (result.committed)
541
534
  this.emitCommitted(result.value.receipt);
542
- const handle = this.handles.get(request.memberId);
543
- if (handle !== undefined) {
544
- await handle.dispose();
545
- this.handles.delete(request.memberId);
546
- }
547
- this.diagnostics.delete(request.memberId);
548
- this.clearMemberNotificationState(request.memberId);
535
+ await this.disposeMemberSession(request.memberId, result.value.member);
549
536
  await this.cleanupRemovedMember(result.value.member);
550
537
  return result.value;
551
538
  });
@@ -560,37 +547,26 @@ let AgentTeam = (() => {
560
547
  }
561
548
  /** Human-only Channel membership grant; it never injects historical Thread bodies. */
562
549
  async joinChannel(request) {
563
- this.requireAccepting();
564
- this.requireWorkspace(request.workspaceId);
550
+ const actor = this.humanCall(request.workspaceId);
565
551
  const ledger = this.requireLedger();
566
552
  if (!ledger.hasCommitted(request.requestId))
567
553
  this.assertChannelMembersAvailable([request.memberId]);
568
- const result = await ledger.joinChannel({ ...request, actor: agentTeamHumanActor() });
554
+ const result = await ledger.joinChannel({ ...request, actor });
569
555
  if (result.committed)
570
556
  this.emitCommitted(result.value.receipt);
571
557
  return result.value;
572
558
  }
573
559
  /** Human-only Channel membership removal and Channel-scoped cleanup. */
574
560
  async removeChannelMember(request) {
575
- this.requireAccepting();
576
- this.requireWorkspace(request.workspaceId);
577
- 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 });
578
563
  if (result.committed)
579
564
  this.emitCommitted(result.value.receipt);
580
565
  return result.value;
581
566
  }
582
567
  /** Human top-level Task creation; unfollowed Agent mentions use the two-send result. */
583
568
  async sendMessage(request) {
584
- this.requireAccepting();
585
- this.requireWorkspace(request.workspaceId);
586
- const metadata = await this.resolveMessageAttachments(request);
587
- const result = await this.requireLedger().sendMessage({
588
- ...request, body: this.appendAttachmentLines(request.body, metadata),
589
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
590
- actor: agentTeamHumanActor(),
591
- });
592
- this.emitCommittedOutcome(result);
593
- return result.value;
569
+ return this.sendMessageAs(this.humanCall(request.workspaceId), request);
594
570
  }
595
571
  /**
596
572
  * Resolve uploaded ids and agent-supplied absolute paths into one attachment
@@ -628,7 +604,7 @@ let AgentTeam = (() => {
628
604
  const trimmed = body.trim();
629
605
  if (metadata.length === 0)
630
606
  return trimmed;
631
- 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)}`);
632
608
  return `${trimmed}\n${lines.join('\n')}`;
633
609
  }
634
610
  /** Upload one composer attachment into the cache; bytes are immutable once written. */
@@ -653,22 +629,12 @@ let AgentTeam = (() => {
653
629
  }
654
630
  /** Human existing-Thread reply; unread and revision conflicts are business outcomes. */
655
631
  async reply(request) {
656
- this.requireAccepting();
657
- this.requireWorkspace(request.workspaceId);
658
- const metadata = await this.resolveMessageAttachments(request);
659
- const result = await this.requireLedger().reply({
660
- ...request, body: this.appendAttachmentLines(request.body, metadata),
661
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
662
- actor: agentTeamHumanActor(),
663
- });
664
- this.emitCommittedOutcome(result);
665
- return result.value;
632
+ return this.replyAs(this.humanCall(request.workspaceId), request);
666
633
  }
667
634
  /** Human's personal Attention operation. */
668
635
  async changeAttention(request) {
669
- this.requireAccepting();
670
- this.requireWorkspace(request.workspaceId);
671
- 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 });
672
638
  if (result.committed)
673
639
  this.emitCommitted(result.value.receipt);
674
640
  return result.value;
@@ -680,9 +646,8 @@ let AgentTeam = (() => {
680
646
  }
681
647
  /** Human's durable, atomically acknowledged Thread read. */
682
648
  async readThread(request) {
683
- this.requireAccepting();
684
- this.requireWorkspace(request.workspaceId);
685
- 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 });
686
651
  if (result.committed)
687
652
  this.emitCommitted(result.value.receipt);
688
653
  return result.value;
@@ -704,37 +669,15 @@ let AgentTeam = (() => {
704
669
  }
705
670
  /** Agent-only top-level Task creation. Workspace identity is verified against the live binding. */
706
671
  async sendMessageForAgent(agent, request) {
707
- this.requireAccepting();
708
- const actor = this.memberActor(agent);
709
- this.requireAgentWorkspace(actor, request.workspaceId);
710
- const metadata = await this.resolveMessageAttachments(request);
711
- const result = await this.requireLedger().sendMessage({
712
- ...request, body: this.appendAttachmentLines(request.body, metadata),
713
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
714
- actor,
715
- });
716
- this.emitCommittedOutcome(result);
717
- return result.value;
672
+ return this.sendMessageAs(this.memberCall(agent, request.workspaceId), request);
718
673
  }
719
674
  /** Agent-only existing-Thread reply. */
720
675
  async replyForAgent(agent, request) {
721
- this.requireAccepting();
722
- const actor = this.memberActor(agent);
723
- this.requireAgentWorkspace(actor, request.workspaceId);
724
- const metadata = await this.resolveMessageAttachments(request);
725
- const result = await this.requireLedger().reply({
726
- ...request, body: this.appendAttachmentLines(request.body, metadata),
727
- ...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
728
- actor,
729
- });
730
- this.emitCommittedOutcome(result);
731
- return result.value;
676
+ return this.replyAs(this.memberCall(agent, request.workspaceId), request);
732
677
  }
733
678
  /** Agent-only personal Attention change. */
734
679
  async changeAttentionForAgent(agent, request) {
735
- this.requireAccepting();
736
- const actor = this.memberActor(agent);
737
- this.requireAgentWorkspace(actor, request.workspaceId);
680
+ const actor = this.memberCall(agent, request.workspaceId);
738
681
  const result = await this.requireLedger().changeAttention({ ...request, actor });
739
682
  if (result.committed)
740
683
  this.emitCommitted(result.value.receipt);
@@ -747,9 +690,7 @@ let AgentTeam = (() => {
747
690
  }
748
691
  /** Agent-only Claim mutation. */
749
692
  async changeClaimForAgent(agent, request) {
750
- this.requireAccepting();
751
- const actor = this.memberActor(agent);
752
- this.requireAgentWorkspace(actor, request.workspaceId);
693
+ const actor = this.memberCall(agent, request.workspaceId);
753
694
  const result = await this.requireLedger().changeClaim({ ...request, actor });
754
695
  this.emitCommittedOutcome(result);
755
696
  return result.value;
@@ -765,9 +706,7 @@ let AgentTeam = (() => {
765
706
  return this.requireLedger().inbox(actor, request);
766
707
  }
767
708
  async readThreadForAgent(agent, request) {
768
- this.requireAccepting();
769
- const actor = this.memberActor(agent);
770
- this.requireAgentWorkspace(actor, request.workspaceId);
709
+ const actor = this.memberCall(agent, request.workspaceId);
771
710
  const result = await this.requireLedger().readThread({ ...request, actor });
772
711
  if (result.committed)
773
712
  this.emitCommitted(result.value.receipt);
@@ -804,6 +743,41 @@ let AgentTeam = (() => {
804
743
  throw new Error(`Agent Member '${memberId}' is not available for Channel membership`);
805
744
  }
806
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
+ }
807
781
  memberActor(agent) {
808
782
  const member = this.memberForAgent(agent);
809
783
  if (member === undefined)
@@ -844,9 +818,16 @@ let AgentTeam = (() => {
844
818
  await this.initializePrivateMemory(member.privateMemoryPath);
845
819
  const persisted = knownSessions !== undefined ? knownSessions.has(member.sessionId)
846
820
  : (await this.ctx.sessionPersistence.list()).some(header => header.id === member.sessionId);
821
+ // AgentOptions declares only provider/model. Install the full selection
822
+ // through the public Agent model-selection seam so reasoning effort is
823
+ // applied to the next request and not lost during activation.
824
+ const selection = member.model ?? this.ctx.agentDefaultModel.currentSelection();
825
+ const agentOptions = { provider: selection.provider, model: selection.model };
826
+ const selected = { current: selection, assembled: undefined };
847
827
  const setup = async (agentCtx) => {
848
828
  await this.ctx.agentPresets.mount(agentCtx, member.presetId);
849
829
  this.validateMemberPreset(agentCtx);
830
+ installModelSelection(agentCtx, selected);
850
831
  return {
851
832
  commit: () => {
852
833
  const agent = agentCtx.agent;
@@ -857,10 +838,6 @@ let AgentTeam = (() => {
857
838
  },
858
839
  };
859
840
  };
860
- // A Member-pinned selection wins; otherwise the Host default applies at
861
- // every activation, so an override survives restarts and a cleared one
862
- // re-reads the live default.
863
- const agentOptions = member.model ?? this.ctx.agentDefaultModel.currentSelection();
864
841
  created = persisted
865
842
  ? await this.ctx.agents.resume({ resumeSessionId: member.sessionId, agentOptions, setup })
866
843
  : await this.ctx.agents.create({
@@ -871,19 +848,51 @@ let AgentTeam = (() => {
871
848
  });
872
849
  await workspace.attachSession(member.sessionId);
873
850
  this.handles.set(member.memberId, created);
874
- this.diagnostics.delete(member.memberId);
851
+ this.modelSelections.set(member.memberId, selected);
852
+ this.clearMemberFailure(member.memberId, 'activation');
875
853
  this.nameMemberSession(member, created.agent);
876
854
  this.notifyMember(created.agent);
855
+ this.autoCompaction.activated(member.memberId);
877
856
  }
878
857
  catch (error) {
879
858
  await created?.dispose();
880
- this.diagnostics.set(member.memberId, error instanceof Error ? error.message : String(error));
859
+ this.modelSelections.delete(member.memberId);
860
+ this.setMemberFailure(member.memberId, 'activation', error instanceof Error ? error.message : String(error));
881
861
  }
882
862
  finally {
883
863
  // Activation only changes this Workspace's presence projection.
884
864
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
885
865
  }
886
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
+ }
887
896
  /**
888
897
  * Default an untitled Member Session to its handle so the ordinary Session
889
898
  * list names it. An explicit rename or any earlier title always wins; the
@@ -938,17 +947,35 @@ let AgentTeam = (() => {
938
947
  return Object.freeze({ member, availability: 'inactive', presence: 'unavailable' });
939
948
  if (member.state === 'suspended')
940
949
  return Object.freeze({ member, availability: 'suspended', presence: 'unavailable' });
941
- const diagnostic = this.diagnostics.get(member.memberId);
942
- if (diagnostic !== undefined)
943
- 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 });
944
953
  const handle = this.handles.get(member.memberId);
945
954
  if (handle === undefined)
946
955
  return Object.freeze({ member, availability: 'unavailable', presence: 'unavailable' });
947
- 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;
948
960
  if (runtimeError !== undefined)
949
961
  return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
950
962
  return Object.freeze({ member, availability: 'active', presence: handle.agent.status === 'running' ? 'working' : 'available' });
951
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
+ }
952
979
  requireWorkspace(workspaceId) {
953
980
  const workspace = this.ctx.workspaceRegistry.get(workspaceId);
954
981
  if (workspace === undefined)
@@ -978,6 +1005,13 @@ let AgentTeam = (() => {
978
1005
  if (handle !== undefined)
979
1006
  this.notifyMember(handle.agent);
980
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 }]);
981
1015
  }
982
1016
  /** Wake from durable unread state with bounded facts for direct and state-changing work. */
983
1017
  notifyMember(agent) {
@@ -994,12 +1028,19 @@ let AgentTeam = (() => {
994
1028
  ]));
995
1029
  if (this.notifiedInbox.get(member.memberId) === signature)
996
1030
  return;
997
- const existingHint = [...agent.inbox.nextStep, ...agent.inbox.nextTurn].find(message => message.source.kind === 'plugin' && message.source.plugin === '@wowyuarm/dsh-agent-team');
998
- if (existingHint !== undefined)
999
- 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);
1000
1041
  const hint = createUserMessage({
1001
- content: [{ type: 'text', text: this.notificationText(notifications) }],
1002
- source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Team Inbox has unread work.' },
1042
+ content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
1043
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: INBOX_NOTICE_SUMMARY },
1003
1044
  });
1004
1045
  this.notifiedInbox.set(member.memberId, signature);
1005
1046
  try {
@@ -1010,7 +1051,17 @@ let AgentTeam = (() => {
1010
1051
  throw error;
1011
1052
  }
1012
1053
  }
1013
- notificationText(notifications) {
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
+ }
1064
+ notificationText(notifications, readerId) {
1014
1065
  const maxCharacters = 32 * 1024;
1015
1066
  const sections = ['Team Inbox has unread work.'];
1016
1067
  let characterCount = sections[0].length;
@@ -1036,19 +1087,19 @@ let AgentTeam = (() => {
1036
1087
  ? 'human' : this.requireLedger().getMember(fact.message.sender)?.handle ?? fact.message.sender;
1037
1088
  const detail = ['Direct Team mention', `From: ${sender}`, `Channel: ${item.channelRef}`,
1038
1089
  `Task: ${item.task.taskRef}`, `Thread: ${item.thread.threadRef}`, `Message ref: ${fact.message.messageRef}`,
1039
- `Revision: ${item.thread.revision}`, `Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
1090
+ `Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
1040
1091
  if (append(detail))
1041
1092
  detailedFactCount += 1;
1042
1093
  }
1043
1094
  else if (fact.kind === 'activity') {
1044
- const detail = `${this.activityNotification(fact.activity)}\nThread: ${item.thread.threadRef}\nRevision: ${item.thread.revision}`;
1095
+ const detail = `${this.activityNotification(fact.activity, readerId)}\nThread: ${item.thread.threadRef}`;
1045
1096
  if (append(detail))
1046
1097
  detailedFactCount += 1;
1047
1098
  }
1048
1099
  }
1049
1100
  const ordinaryCount = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct).length;
1050
1101
  if (ordinaryCount > 0)
1051
- append(`Task ${item.task.taskRef}: ${ordinaryCount} unread update${ordinaryCount === 1 ? '' : 's'}; revision ${item.thread.revision}.`);
1102
+ append(`Task ${item.task.taskRef}: ${ordinaryCount} unread update${ordinaryCount === 1 ? '' : 's'}.`);
1052
1103
  }
1053
1104
  if (omitted)
1054
1105
  sections.push('More unread work remains in team_inbox; the automatic context is bounded.');
@@ -1059,9 +1110,15 @@ let AgentTeam = (() => {
1059
1110
  const limit = 8 * 1024;
1060
1111
  return body.length <= limit ? body : `${body.slice(0, limit)}\n[Message body truncated; use team_thread read for the full Message.]`;
1061
1112
  }
1062
- activityNotification(activity) {
1113
+ activityNotification(activity, readerId) {
1063
1114
  const actor = activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID
1064
1115
  ? 'human' : this.requireLedger().getMember(activity.actor)?.handle ?? activity.actor;
1116
+ // Early acceptance completes the reader's own open Claim inside the accept
1117
+ // operation: say so plainly, or the owner keeps working on a done Task.
1118
+ if (activity.kind === 'accept' && activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID && readerId !== undefined
1119
+ && activity.completedClaimRefs?.some(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId)) {
1120
+ return `Team Task update\n${actor} accepted Task ${activity.taskRef} and your open Claim was completed with it. No further work is needed.`;
1121
+ }
1065
1122
  if (activity.kind === 'claim' || activity.kind === 'done' || activity.kind === 'release') {
1066
1123
  return `Team Task update\n${actor} ${activity.kind} Claim ${activity.claimRef} on Task ${activity.taskRef}.`;
1067
1124
  }
@@ -1072,6 +1129,22 @@ let AgentTeam = (() => {
1072
1129
  ? ` Released Claims: ${activity.releasedClaimRefs.join(', ')}.` : '';
1073
1130
  return `Team Task update\n${actor} ${activity.kind} Task ${activity.taskRef}.${released}`;
1074
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
+ }
1075
1148
  clearMemberNotificationState(memberId) {
1076
1149
  this.notifiedInbox.delete(memberId);
1077
1150
  }