@wowyuarm/dsh-agent-team 0.1.0 → 0.1.2

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 (91) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +10 -2
  3. package/package.json +32 -24
  4. package/packages/agent-team/lib/attachments.js +157 -0
  5. package/packages/agent-team/lib/index.js +280 -22
  6. package/packages/agent-team/lib/ledger.js +98 -22
  7. package/packages/agent-team/lib/recovery.js +87 -0
  8. package/packages/agent-team/lib/spec.js +27 -5
  9. package/packages/agent-team/lib/typert.host.js +313 -26
  10. package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
  11. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  12. package/packages/agent-team/lib/typert.remote-client.js +234 -17
  13. package/packages/agent-team/lib/types/attachments.d.ts +69 -0
  14. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
  15. package/packages/agent-team/lib/types/index.d.ts +68 -6
  16. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  17. package/packages/agent-team/lib/types/ledger.d.ts +17 -1
  18. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/recovery.d.ts +42 -0
  20. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
  21. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/types.d.ts +94 -3
  23. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  24. package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
  25. package/packages/client-agent-team/README.md +4 -3
  26. package/packages/client-agent-team/README.zh.md +4 -3
  27. package/packages/client-agent-team/lib/client.js +1791 -322
  28. package/packages/client-agent-team/lib/client.js.map +1 -1
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +5 -2
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  31. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +108 -59
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +7 -1
  33. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +80 -19
  35. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +38 -19
  37. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
  38. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamComposer.js +61 -4
  40. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +12 -2
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +152 -14
  46. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
  47. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
  48. package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
  49. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +7 -0
  50. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  51. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +93 -16
  52. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
  55. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
  56. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
  57. package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
  58. package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
  59. package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
  60. package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
  61. package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
  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 +50 -10
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts +34 -0
  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 +34 -0
  67. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
  68. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
  70. package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
  71. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
  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 +18 -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 +20 -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 +58 -0
  84. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +15 -0
  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 +30 -2
  87. package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
  88. package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
  89. package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
  90. package/packages/tool-agent-team/lib/index.js +10 -6
  91. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -44,13 +44,16 @@ 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';
54
+ import { ATTACHMENT_MAX_BYTES, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
53
55
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
56
+ import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_ATTEMPTS } from "./recovery.js";
54
57
  import { agentTeamDomainSpec } from "./spec.js";
55
58
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
56
59
  export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
@@ -81,16 +84,20 @@ export const AGENT_TEAM_TOOL_NAMES = Object.freeze([
81
84
  let AgentTeam = (() => {
82
85
  let _classSuper = TypertRemoteService;
83
86
  let _instanceExtraInitializers = [];
87
+ let _resolveTaskRefs_decorators;
84
88
  let _membersForClient_decorators;
85
89
  let _changes_decorators;
86
90
  let _createChannel_decorators;
87
91
  let _updateChannel_decorators;
88
92
  let _addMember_decorators;
93
+ let _recoverMember_decorators;
89
94
  let _updateMember_decorators;
90
95
  let _changeTask_decorators;
91
96
  let _joinChannel_decorators;
92
97
  let _removeChannelMember_decorators;
93
98
  let _sendMessage_decorators;
99
+ let _putAttachment_decorators;
100
+ let _getAttachment_decorators;
94
101
  let _reply_decorators;
95
102
  let _changeAttention_decorators;
96
103
  let _inbox_decorators;
@@ -101,16 +108,20 @@ let AgentTeam = (() => {
101
108
  return class AgentTeam extends _classSuper {
102
109
  static {
103
110
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
111
+ _resolveTaskRefs_decorators = [Remote('resolveTaskRefs')];
104
112
  _membersForClient_decorators = [Remote('members')];
105
113
  _changes_decorators = [Remote('changes')];
106
114
  _createChannel_decorators = [Remote('createChannel')];
107
115
  _updateChannel_decorators = [Remote('updateChannel')];
108
116
  _addMember_decorators = [Remote('addMember')];
117
+ _recoverMember_decorators = [Remote('recoverMember')];
109
118
  _updateMember_decorators = [Remote('updateMember')];
110
119
  _changeTask_decorators = [Remote('changeTask')];
111
120
  _joinChannel_decorators = [Remote('joinChannel')];
112
121
  _removeChannelMember_decorators = [Remote('removeChannelMember')];
113
122
  _sendMessage_decorators = [Remote('sendMessage')];
123
+ _putAttachment_decorators = [Remote('putAttachment')];
124
+ _getAttachment_decorators = [Remote('getAttachment')];
114
125
  _reply_decorators = [Remote('reply')];
115
126
  _changeAttention_decorators = [Remote('changeAttention')];
116
127
  _inbox_decorators = [Remote('inbox')];
@@ -118,16 +129,20 @@ let AgentTeam = (() => {
118
129
  _threadObservations_decorators = [Remote('threadObservations')];
119
130
  _threadHistory_decorators = [Remote('threadHistory')];
120
131
  _view_decorators = [Remote('view')];
132
+ __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);
121
133
  __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);
122
134
  __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);
123
135
  __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);
124
136
  __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);
125
137
  __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);
138
+ __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);
126
139
  __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);
127
140
  __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);
128
141
  __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);
129
142
  __esDecorate(this, null, _removeChannelMember_decorators, { kind: "method", name: "removeChannelMember", static: false, private: false, access: { has: obj => "removeChannelMember" in obj, get: obj => obj.removeChannelMember }, metadata: _metadata }, null, _instanceExtraInitializers);
130
143
  __esDecorate(this, null, _sendMessage_decorators, { kind: "method", name: "sendMessage", static: false, private: false, access: { has: obj => "sendMessage" in obj, get: obj => obj.sendMessage }, metadata: _metadata }, null, _instanceExtraInitializers);
144
+ __esDecorate(this, null, _putAttachment_decorators, { kind: "method", name: "putAttachment", static: false, private: false, access: { has: obj => "putAttachment" in obj, get: obj => obj.putAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
145
+ __esDecorate(this, null, _getAttachment_decorators, { kind: "method", name: "getAttachment", static: false, private: false, access: { has: obj => "getAttachment" in obj, get: obj => obj.getAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
131
146
  __esDecorate(this, null, _reply_decorators, { kind: "method", name: "reply", static: false, private: false, access: { has: obj => "reply" in obj, get: obj => obj.reply }, metadata: _metadata }, null, _instanceExtraInitializers);
132
147
  __esDecorate(this, null, _changeAttention_decorators, { kind: "method", name: "changeAttention", static: false, private: false, access: { has: obj => "changeAttention" in obj, get: obj => obj.changeAttention }, metadata: _metadata }, null, _instanceExtraInitializers);
133
148
  __esDecorate(this, null, _inbox_decorators, { kind: "method", name: "inbox", static: false, private: false, access: { has: obj => "inbox" in obj, get: obj => obj.inbox }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -149,9 +164,23 @@ let AgentTeam = (() => {
149
164
  domain = __runInitializers(this, _instanceExtraInitializers);
150
165
  ledger;
151
166
  handles = new Map();
167
+ /** Live selection refs let model edits take effect without disposing the Session. */
168
+ modelSelections = new Map();
152
169
  diagnostics = new Map();
153
170
  runtimeErrors = new Map();
171
+ /** Agent ids with a turn in flight; restarts must wait for the boundary. */
172
+ runningAgents = new Set();
154
173
  notifiedInbox = new Map();
174
+ attachmentGcTimer;
175
+ 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);
179
+ },
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`);
182
+ },
183
+ });
155
184
  lifecycleTail = Promise.resolve();
156
185
  accepting = true;
157
186
  changeVersion = 0;
@@ -165,11 +194,20 @@ let AgentTeam = (() => {
165
194
  const member = this.memberForAgent(agent);
166
195
  if (member === undefined)
167
196
  return;
168
- this.runtimeErrors.set(agent.id, error instanceof Error ? error.message : String(error));
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ this.runtimeErrors.set(agent.id, message);
199
+ const kind = classifyRecoverableError(message);
200
+ if (kind !== undefined)
201
+ this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable error (${kind}); automatic recovery scheduled`);
202
+ this.recovery.onError(member.memberId, message);
169
203
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
170
204
  });
171
205
  this.ctx.on('agent/status', ({ agent, status }) => {
172
206
  const member = this.memberForAgent(agent);
207
+ if (status === 'running')
208
+ this.runningAgents.add(agent.id);
209
+ else
210
+ this.runningAgents.delete(agent.id);
173
211
  if (status === 'running' && member !== undefined) {
174
212
  const recovered = this.runtimeErrors.delete(agent.id);
175
213
  if (recovered)
@@ -177,14 +215,24 @@ let AgentTeam = (() => {
177
215
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
178
216
  this.notifyMember(agent);
179
217
  }
218
+ // A turn that ends without an error closes any automatic recovery episode.
219
+ if (status === 'idle' && member !== undefined && !this.runtimeErrors.has(agent.id)) {
220
+ this.recovery.onCleanTurnEnd(member.memberId);
221
+ }
180
222
  });
181
223
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
182
224
  this.ctx.effect(() => async () => {
183
225
  this.accepting = false;
226
+ this.recovery.dispose();
227
+ if (this.attachmentGcTimer !== undefined)
228
+ clearInterval(this.attachmentGcTimer);
229
+ this.attachmentGcTimer = undefined;
184
230
  this.emitChanged();
185
231
  await this.lifecycleTail;
186
232
  await Promise.all([...this.handles.values()].map(handle => handle.dispose()));
187
233
  this.handles.clear();
234
+ this.modelSelections.clear();
235
+ this.runningAgents.clear();
188
236
  await domain.close();
189
237
  }, 'agentTeam.dispose');
190
238
  this.domain = domain;
@@ -193,6 +241,7 @@ let AgentTeam = (() => {
193
241
  const initialization = await ledger.initialize();
194
242
  if (initialization.committed)
195
243
  this.emitCommitted(initialization.value);
244
+ this.startAttachmentGc(ledger);
196
245
  // One metadata listing serves every Member restore; per-member list calls
197
246
  // would repeat the same I/O linearly during startup.
198
247
  const persistedSessions = new Set((await this.ctx.sessionPersistence.list()).map(header => header.id));
@@ -216,6 +265,18 @@ let AgentTeam = (() => {
216
265
  return this.requireLedger().listMembers().map(member => this.memberStatus(member));
217
266
  }
218
267
  /** Return only this Workspace's current Member projection to the Client. */
268
+ /** Read-only navigation lookup for branded Task refs found in message bodies. */
269
+ resolveTaskRefs(request) {
270
+ this.requireWorkspace(request.workspaceId);
271
+ const seen = new Set();
272
+ const taskRefs = request.taskRefs.filter(taskRef => {
273
+ if (seen.has(taskRef))
274
+ return false;
275
+ seen.add(taskRef);
276
+ return true;
277
+ });
278
+ return Object.freeze({ resolved: Object.freeze(this.requireLedger().resolveTaskRefs(request.workspaceId, taskRefs)) });
279
+ }
219
280
  membersForClient(request) {
220
281
  this.requireWorkspace(request.workspaceId);
221
282
  return this.members()
@@ -293,6 +354,7 @@ let AgentTeam = (() => {
293
354
  async addMember(request) {
294
355
  return this.enqueueLifecycle(async () => {
295
356
  const workspace = this.requireWorkspace(request.workspaceId);
357
+ await this.assertModelRoute(request.model);
296
358
  const memberId = `member:${randomUUID()}`;
297
359
  const member = Object.freeze({
298
360
  memberId,
@@ -325,6 +387,7 @@ let AgentTeam = (() => {
325
387
  await handle.dispose();
326
388
  this.handles.delete(request.memberId);
327
389
  }
390
+ this.modelSelections.delete(request.memberId);
328
391
  this.diagnostics.delete(request.memberId);
329
392
  this.clearMemberNotificationState(request.memberId);
330
393
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
@@ -342,13 +405,96 @@ let AgentTeam = (() => {
342
405
  });
343
406
  }
344
407
  /**
345
- * Human edit of one Member's mutable facts. A model change restarts an
346
- * active Member: single-Session Members read their selection only at
347
- * activation, so the quiescing dispose plus reactivation applies the new
348
- * provider/model immediately instead of at the next Host restart.
408
+ * Operator nudge for an error-stopped Member: steer a continuation prompt
409
+ * into its live session. Runtime-only no ledger operation, no suspend.
410
+ * Taking over manually also cancels any pending automatic recovery episode.
411
+ */
412
+ async recoverMember(request) {
413
+ this.requireAccepting();
414
+ const member = this.requireLedger().getMember(request.memberId);
415
+ if (member === undefined || member.workspaceId !== request.workspaceId)
416
+ throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
417
+ const storedError = this.storedErrorFor(member.memberId);
418
+ this.recovery.stopTracking(request.memberId);
419
+ const kind = classifyRecoverableError(storedError ?? '');
420
+ this.ctx.logger.info(`agent-team: operator asked member '${member.handle}' to resume`);
421
+ this.steerResume(member, this.resumeText(kind ?? 'unspecified failure', { operatorRequested: true }));
422
+ return Object.freeze({ status: this.memberStatus(member) });
423
+ }
424
+ /** Ledger handle for log lines; falls back to the raw id when unknown. */
425
+ memberLabel(memberId) {
426
+ return this.ledger?.getMember(memberId)?.handle ?? memberId;
427
+ }
428
+ /**
429
+ * Cache GC: uploads referenced by a Message survive 72h from upload so
430
+ * Member agents keep a consumption window; orphans (never sent) go after
431
+ * 24h. Runs once at startup and then daily — in-process only, because the
432
+ * cache is transient by design and rebuilds nothing across restarts.
433
+ */
434
+ startAttachmentGc(ledger) {
435
+ const sweep = async () => {
436
+ await sweepAttachmentCache(attachmentsRoot(), ledger.referencedAttachmentIds(), Date.now());
437
+ };
438
+ void sweep();
439
+ this.attachmentGcTimer = setInterval(() => { void sweep(); }, 24 * 60 * 60 * 1000);
440
+ this.attachmentGcTimer.unref?.();
441
+ }
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);
448
+ }
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;
462
+ }
463
+ /**
464
+ * Steer one continuation message into a Member's live session. Throws when
465
+ * no handle exists so the coordinator stops tracking; appends the inbox
466
+ * snapshot whenever there is anything new to read.
467
+ */
468
+ steerResume(member, text) {
469
+ const handle = this.handles.get(member.memberId);
470
+ if (handle === undefined)
471
+ throw new Error(`member '${member.handle}' has no active session`);
472
+ const notifications = this.requireLedger().notificationFacts(member.memberId, { workspaceId: member.workspaceId });
473
+ const body = notifications.length === 0 ? text : `${text}\n\n${this.notificationText(notifications, member.memberId)}`;
474
+ const hint = createUserMessage({
475
+ content: [{ type: 'text', text: body }],
476
+ source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Recovery: continue your interrupted work.' },
477
+ });
478
+ handle.agent.steer(hint);
479
+ }
480
+ /** Automatic-recovery injection; throwing tells the coordinator the target is gone. */
481
+ injectRecovery(memberId, attempt, kind) {
482
+ const handle = this.handles.get(memberId);
483
+ const agent = handle?.agent;
484
+ const member = agent !== undefined ? this.memberForAgent(agent) : undefined;
485
+ if (agent === undefined || member === undefined || member.state !== 'enabled')
486
+ throw new Error(`member '${memberId}' cannot be recovered automatically`);
487
+ this.steerResume(member, this.resumeText(kind, { attempt }));
488
+ }
489
+ /**
490
+ * Human edit of one Member's mutable facts. A live model selection is
491
+ * updated in place: disposing an Agent emits session/disposed, which makes
492
+ * the Web Client permanently mark the same Session id unavailable even when
493
+ * Team immediately recreates it.
349
494
  */
350
495
  async updateMember(request) {
351
496
  return this.enqueueLifecycle(async () => {
497
+ await this.assertModelRoute(request.model);
352
498
  const previous = this.requireLedger().getMember(request.memberId);
353
499
  const result = await this.requireLedger().updateMember({ ...request, actor: agentTeamHumanActor() });
354
500
  if (result.committed)
@@ -356,9 +502,10 @@ let AgentTeam = (() => {
356
502
  const stored = result.value.member;
357
503
  const active = this.handles.get(request.memberId);
358
504
  if (active !== undefined && !isDeepStrictEqual(previous?.model ?? undefined, stored.model ?? undefined)) {
359
- await active.dispose();
360
- this.handles.delete(request.memberId);
361
- await this.activateMember(stored);
505
+ const selection = this.modelSelections.get(request.memberId);
506
+ if (selection === undefined)
507
+ throw new Error(`Agent Member '${stored.handle}' has no live model selection`);
508
+ selection.current = stored.model ?? this.ctx.agentDefaultModel.currentSelection();
362
509
  }
363
510
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
364
511
  });
@@ -374,6 +521,7 @@ let AgentTeam = (() => {
374
521
  await handle.dispose();
375
522
  this.handles.delete(request.memberId);
376
523
  }
524
+ this.modelSelections.delete(request.memberId);
377
525
  this.diagnostics.delete(request.memberId);
378
526
  this.clearMemberNotificationState(request.memberId);
379
527
  await this.cleanupRemovedMember(result.value.member);
@@ -413,15 +561,84 @@ let AgentTeam = (() => {
413
561
  async sendMessage(request) {
414
562
  this.requireAccepting();
415
563
  this.requireWorkspace(request.workspaceId);
416
- const result = await this.requireLedger().sendMessage({ ...request, actor: agentTeamHumanActor() });
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
+ });
417
570
  this.emitCommittedOutcome(result);
418
571
  return result.value;
419
572
  }
573
+ /**
574
+ * Resolve uploaded ids and agent-supplied absolute paths into one attachment
575
+ * metadata list. Paths are all validated before anything is copied, so one
576
+ * rejection leaves the cache untouched and the message uncommitted.
577
+ */
578
+ async resolveMessageAttachments(request) {
579
+ const fromPaths = [];
580
+ if (request.attachmentPaths !== undefined && request.attachmentPaths.length > 0) {
581
+ for (const absolutePath of request.attachmentPaths)
582
+ await validatePathAttachment(absolutePath);
583
+ for (const absolutePath of request.attachmentPaths)
584
+ fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath)));
585
+ }
586
+ return [...fromPaths, ...await this.prepareAttachments(request.attachments)];
587
+ }
588
+ /** Verify requested attachment ids against the cache. */
589
+ async prepareAttachments(requested) {
590
+ if (requested === undefined || requested.length === 0)
591
+ return [];
592
+ const metadata = [];
593
+ for (const attachmentId of requested) {
594
+ const stored = await readAttachment(attachmentsRoot(), attachmentId);
595
+ if (stored === undefined)
596
+ throw new Error(`attachment '${attachmentId}' is not in the upload cache`);
597
+ metadata.push(Object.freeze({ attachmentId, name: stored.name, byteSize: stored.byteSize, mediaType: stored.mediaType }));
598
+ }
599
+ return metadata;
600
+ }
601
+ /**
602
+ * Derive the stored body: one machine-facing `[attachment] <absolute path>`
603
+ * line per attachment appended to the member-facing text.
604
+ */
605
+ appendAttachmentLines(body, metadata) {
606
+ const trimmed = body.trim();
607
+ if (metadata.length === 0)
608
+ return trimmed;
609
+ const lines = metadata.map(attachment => `[attachment] ${dshHomePath('agent-team', 'attachments', 'v1', attachment.attachmentId, attachment.name)}`);
610
+ return `${trimmed}\n${lines.join('\n')}`;
611
+ }
612
+ /** Upload one composer attachment into the cache; bytes are immutable once written. */
613
+ async putAttachment(request) {
614
+ this.requireAccepting();
615
+ this.requireWorkspace(request.workspaceId);
616
+ const bytes = Buffer.from(request.bytesBase64, 'base64');
617
+ if (bytes.byteLength === 0)
618
+ throw new Error('attachment must not be empty');
619
+ if (bytes.byteLength > ATTACHMENT_MAX_BYTES)
620
+ throw new Error(`attachment exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
621
+ const mediaType = sanitizeMediaType(request.mediaType);
622
+ const attachmentId = newAttachmentId();
623
+ return Object.freeze(await writeAttachment(attachmentsRoot(), attachmentId, request.name, mediaType, bytes));
624
+ }
625
+ /** Read one cached attachment back for client display; gone entries throw and the UI degrades to a chip. */
626
+ async getAttachment(request) {
627
+ const stored = await readAttachment(attachmentsRoot(), request.attachmentId);
628
+ if (stored === undefined)
629
+ throw new Error(`attachment '${request.attachmentId}' is no longer cached`);
630
+ return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
631
+ }
420
632
  /** Human existing-Thread reply; unread and revision conflicts are business outcomes. */
421
633
  async reply(request) {
422
634
  this.requireAccepting();
423
635
  this.requireWorkspace(request.workspaceId);
424
- const result = await this.requireLedger().reply({ ...request, actor: agentTeamHumanActor() });
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
+ });
425
642
  this.emitCommittedOutcome(result);
426
643
  return result.value;
427
644
  }
@@ -468,7 +685,12 @@ let AgentTeam = (() => {
468
685
  this.requireAccepting();
469
686
  const actor = this.memberActor(agent);
470
687
  this.requireAgentWorkspace(actor, request.workspaceId);
471
- const result = await this.requireLedger().sendMessage({ ...request, actor });
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
+ });
472
694
  this.emitCommittedOutcome(result);
473
695
  return result.value;
474
696
  }
@@ -477,7 +699,12 @@ let AgentTeam = (() => {
477
699
  this.requireAccepting();
478
700
  const actor = this.memberActor(agent);
479
701
  this.requireAgentWorkspace(actor, request.workspaceId);
480
- const result = await this.requireLedger().reply({ ...request, actor });
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
+ });
481
708
  this.emitCommittedOutcome(result);
482
709
  return result.value;
483
710
  }
@@ -565,6 +792,26 @@ let AgentTeam = (() => {
565
792
  if (this.requireLedger().getMember(actor.memberId)?.workspaceId !== workspaceId)
566
793
  throw new Error('Member cannot mutate another Workspace');
567
794
  }
795
+ /**
796
+ * Validate a pinned model route's reasoning effort against the adapter's own
797
+ * metadata when the LLM service is reachable; unknown routes defer to the
798
+ * LLM layer's runtime check at call time.
799
+ */
800
+ async assertModelRoute(model) {
801
+ if (model === undefined || model.reasoningEffort === undefined)
802
+ return;
803
+ try {
804
+ const resolved = await this.ctx.llm.resolveModelInfo(model.provider, model.model);
805
+ const efforts = resolved.reasoning?.efforts ?? [];
806
+ if (efforts.length > 0 && !efforts.some(effort => effort.id === model.reasoningEffort)) {
807
+ throw new Error(`reasoning effort '${model.reasoningEffort}' is not supported by ${model.provider}/${model.model}`);
808
+ }
809
+ }
810
+ catch (error) {
811
+ if (error instanceof Error && error.message.includes('is not supported by'))
812
+ throw error;
813
+ }
814
+ }
568
815
  async activateMember(member, knownWorkspacePath, knownSessions) {
569
816
  if (this.handles.has(member.memberId))
570
817
  return;
@@ -575,9 +822,16 @@ let AgentTeam = (() => {
575
822
  await this.initializePrivateMemory(member.privateMemoryPath);
576
823
  const persisted = knownSessions !== undefined ? knownSessions.has(member.sessionId)
577
824
  : (await this.ctx.sessionPersistence.list()).some(header => header.id === member.sessionId);
825
+ // AgentOptions declares only provider/model. Install the full selection
826
+ // through the public Agent model-selection seam so reasoning effort is
827
+ // applied to the next request and not lost during activation.
828
+ const selection = member.model ?? this.ctx.agentDefaultModel.currentSelection();
829
+ const agentOptions = { provider: selection.provider, model: selection.model };
830
+ const selected = { current: selection, assembled: undefined };
578
831
  const setup = async (agentCtx) => {
579
832
  await this.ctx.agentPresets.mount(agentCtx, member.presetId);
580
833
  this.validateMemberPreset(agentCtx);
834
+ installModelSelection(agentCtx, selected);
581
835
  return {
582
836
  commit: () => {
583
837
  const agent = agentCtx.agent;
@@ -588,10 +842,6 @@ let AgentTeam = (() => {
588
842
  },
589
843
  };
590
844
  };
591
- // A Member-pinned selection wins; otherwise the Host default applies at
592
- // every activation, so an override survives restarts and a cleared one
593
- // re-reads the live default.
594
- const agentOptions = member.model ?? this.ctx.agentDefaultModel.currentSelection();
595
845
  created = persisted
596
846
  ? await this.ctx.agents.resume({ resumeSessionId: member.sessionId, agentOptions, setup })
597
847
  : await this.ctx.agents.create({
@@ -602,12 +852,14 @@ let AgentTeam = (() => {
602
852
  });
603
853
  await workspace.attachSession(member.sessionId);
604
854
  this.handles.set(member.memberId, created);
855
+ this.modelSelections.set(member.memberId, selected);
605
856
  this.diagnostics.delete(member.memberId);
606
857
  this.nameMemberSession(member, created.agent);
607
858
  this.notifyMember(created.agent);
608
859
  }
609
860
  catch (error) {
610
861
  await created?.dispose();
862
+ this.modelSelections.delete(member.memberId);
611
863
  this.diagnostics.set(member.memberId, error instanceof Error ? error.message : String(error));
612
864
  }
613
865
  finally {
@@ -729,7 +981,7 @@ let AgentTeam = (() => {
729
981
  if (existingHint !== undefined)
730
982
  agent.inbox.remove(existingHint.id);
731
983
  const hint = createUserMessage({
732
- content: [{ type: 'text', text: this.notificationText(notifications) }],
984
+ content: [{ type: 'text', text: this.notificationText(notifications, member.memberId) }],
733
985
  source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Team Inbox has unread work.' },
734
986
  });
735
987
  this.notifiedInbox.set(member.memberId, signature);
@@ -741,7 +993,7 @@ let AgentTeam = (() => {
741
993
  throw error;
742
994
  }
743
995
  }
744
- notificationText(notifications) {
996
+ notificationText(notifications, readerId) {
745
997
  const maxCharacters = 32 * 1024;
746
998
  const sections = ['Team Inbox has unread work.'];
747
999
  let characterCount = sections[0].length;
@@ -767,19 +1019,19 @@ let AgentTeam = (() => {
767
1019
  ? 'human' : this.requireLedger().getMember(fact.message.sender)?.handle ?? fact.message.sender;
768
1020
  const detail = ['Direct Team mention', `From: ${sender}`, `Channel: ${item.channelRef}`,
769
1021
  `Task: ${item.task.taskRef}`, `Thread: ${item.thread.threadRef}`, `Message ref: ${fact.message.messageRef}`,
770
- `Revision: ${item.thread.revision}`, `Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
1022
+ `Message: ${this.boundedNotificationBody(fact.message.body)}`].join('\n');
771
1023
  if (append(detail))
772
1024
  detailedFactCount += 1;
773
1025
  }
774
1026
  else if (fact.kind === 'activity') {
775
- const detail = `${this.activityNotification(fact.activity)}\nThread: ${item.thread.threadRef}\nRevision: ${item.thread.revision}`;
1027
+ const detail = `${this.activityNotification(fact.activity, readerId)}\nThread: ${item.thread.threadRef}`;
776
1028
  if (append(detail))
777
1029
  detailedFactCount += 1;
778
1030
  }
779
1031
  }
780
1032
  const ordinaryCount = facts.filter(entry => entry.fact.kind === 'message' && !entry.direct).length;
781
1033
  if (ordinaryCount > 0)
782
- append(`Task ${item.task.taskRef}: ${ordinaryCount} unread update${ordinaryCount === 1 ? '' : 's'}; revision ${item.thread.revision}.`);
1034
+ append(`Task ${item.task.taskRef}: ${ordinaryCount} unread update${ordinaryCount === 1 ? '' : 's'}.`);
783
1035
  }
784
1036
  if (omitted)
785
1037
  sections.push('More unread work remains in team_inbox; the automatic context is bounded.');
@@ -790,9 +1042,15 @@ let AgentTeam = (() => {
790
1042
  const limit = 8 * 1024;
791
1043
  return body.length <= limit ? body : `${body.slice(0, limit)}\n[Message body truncated; use team_thread read for the full Message.]`;
792
1044
  }
793
- activityNotification(activity) {
1045
+ activityNotification(activity, readerId) {
794
1046
  const actor = activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID
795
1047
  ? 'human' : this.requireLedger().getMember(activity.actor)?.handle ?? activity.actor;
1048
+ // Early acceptance completes the reader's own open Claim inside the accept
1049
+ // operation: say so plainly, or the owner keeps working on a done Task.
1050
+ if (activity.kind === 'accept' && activity.actor === AGENT_TEAM_HUMAN_MEMBER_ID && readerId !== undefined
1051
+ && activity.completedClaimRefs?.some(claimRef => this.requireLedger().getClaim(claimRef)?.owner === readerId)) {
1052
+ return `Team Task update\n${actor} accepted Task ${activity.taskRef} and your open Claim was completed with it. No further work is needed.`;
1053
+ }
796
1054
  if (activity.kind === 'claim' || activity.kind === 'done' || activity.kind === 'release') {
797
1055
  return `Team Task update\n${actor} ${activity.kind} Claim ${activity.claimRef} on Task ${activity.taskRef}.`;
798
1056
  }