@wowyuarm/dsh-agent-team 0.1.0 → 0.1.1

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 (81) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +10 -2
  3. package/package.json +2 -2
  4. package/packages/agent-team/lib/attachments.js +157 -0
  5. package/packages/agent-team/lib/index.js +274 -5
  6. package/packages/agent-team/lib/ledger.js +65 -13
  7. package/packages/agent-team/lib/recovery.js +87 -0
  8. package/packages/agent-team/lib/spec.js +17 -1
  9. package/packages/agent-team/lib/typert.host.js +321 -21
  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 +249 -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 +66 -1
  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 +85 -2
  23. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  24. package/packages/client-agent-team/README.md +2 -2
  25. package/packages/client-agent-team/README.zh.md +2 -2
  26. package/packages/client-agent-team/lib/client.js +1176 -281
  27. package/packages/client-agent-team/lib/client.js.map +1 -1
  28. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +6 -2
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +104 -58
  31. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +5 -1
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  33. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +44 -19
  34. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  35. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -18
  36. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
  37. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  38. package/packages/client-agent-team/lib/types/client/TeamComposer.js +40 -4
  39. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  40. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  42. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -2
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.js +34 -8
  45. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
  46. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
  47. package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
  48. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +3 -0
  49. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +18 -11
  51. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
  54. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
  55. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
  56. package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
  57. package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
  58. package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
  59. package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
  60. package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
  61. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/index.js +50 -10
  63. package/packages/client-agent-team/lib/types/client/locales.d.ts +28 -0
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/locales.js +28 -0
  66. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
  67. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
  68. package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
  69. package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
  70. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
  72. package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
  73. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  74. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +2 -0
  75. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/team-formatters.js +4 -0
  77. package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
  78. package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
  79. package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
  80. package/packages/tool-agent-team/lib/index.js +6 -2
  81. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
package/README.md CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  [English](README.md) | [简体中文](README.zh.md)
4
4
 
5
- An Agent Team plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): persistent Workspaces, Channels, Threads, Tasks, and managed Agent members in one DSH home.
5
+ [![npm](https://img.shields.io/npm/v/@wowyuarm/dsh-agent-team?style=flat-square)](https://www.npmjs.com/package/@wowyuarm/dsh-agent-team)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
7
+ [![Release](https://img.shields.io/github/v/release/wowyuarm/dsh-agent-team?include_prereleases&style=flat-square)](https://github.com/wowyuarm/dsh-agent-team/releases)
6
8
 
7
- Opt-in by design: install it only where Team mode is needed; ordinary DSH sessions keep their normal preset roster.
9
+ **dsh-agent-team** helps a human manage tasks in an orderly way and use agents as true collaborators: agents are persistent identities for their sessions; Workspaces organize agents and sessions per project; Channels carry responsibilities; Task Threads chain session agents into one line of progress.
10
+
11
+ An opt-in plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): install it only where Team mode is needed; ordinary DSH sessions keep their normal preset roster.
8
12
 
9
13
  ## Preview
10
14
 
@@ -53,6 +57,14 @@ Team mode
53
57
 
54
58
  Create an Agent only in a trusted Workspace. The Team Member preset intentionally grants managed Agent Sessions `danger-full-access`.
55
59
 
60
+ ## Uninstall
61
+
62
+ Remove the bundle from the profile; this also removes its composed layers:
63
+
64
+ ```sh
65
+ dsh plugin --profile web remove @wowyuarm/dsh-agent-team
66
+ ```
67
+
56
68
  ## What it adds
57
69
 
58
70
  - A durable single-host Team with Channels, Messages, Tasks, Threads, Claims, and Agent membership.
package/README.zh.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- 这是一个为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 提供的 Agent Team 插件:在一个 DSH home 中组织持久化的 Workspace、Channel、Thread、Task 和由 Team 管理的 Agent 成员。
5
+ **dsh-agent-team** 帮助 human 有序管理任务、把 agents 用成真正的协作者:Agent session 的持久身份;Workspace 按项目组织 agents 与 sessions;Channel 承载职责分派;Task Thread 把多个 session agent 串成一条推进线。
6
6
 
7
- 按需启用:只在需要 Team mode 的 profile 中安装;普通 DSH Session 保持原有 preset roster。
7
+ 一个为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 提供的按需启用插件:只在需要 Team mode 的 profile 安装,普通 DSH Session 保持原有 preset roster。
8
8
 
9
9
  ## 预览
10
10
 
@@ -53,6 +53,14 @@ Team mode
53
53
 
54
54
  只在可信 Workspace 中创建 Agent。Team Member preset 会给被管理的 Agent Session 授予 `danger-full-access`。
55
55
 
56
+ ## 卸载
57
+
58
+ 从 profile 移除 bundle,同时会移除它组合进来的层:
59
+
60
+ ```sh
61
+ dsh plugin --profile web remove @wowyuarm/dsh-agent-team
62
+ ```
63
+
56
64
  ## 提供的能力
57
65
 
58
66
  - 持久化的单 Host Team,包含 Channel、Message、Task、Thread、Claim 和 Agent membership。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wowyuarm/dsh-agent-team",
3
- "version": "0.1.0",
4
- "description": "Agent Team plugin for DeepSeek Harness",
3
+ "version": "0.1.1",
4
+ "description": "Help humans organize tasks and let agents collaborate - a Team plugin for DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/wowyuarm/dsh-agent-team.git"
@@ -0,0 +1,157 @@
1
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { basename, extname, isAbsolute, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
5
+ /**
6
+ * Composer attachments are a cache, not an archive: bytes live only so Member
7
+ * agents can read them within the consumption window, while the ledger keeps
8
+ * the metadata forever. Everything here derives from the on-disk layout
9
+ * `$DSH_HOME/agent-team/attachments/v1/<attachmentId>/` holding the payload
10
+ * file (original sanitized name) plus a `meta.json` sidecar.
11
+ */
12
+ export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
13
+ /** Referenced uploads survive this long after upload for member consumption. */
14
+ export const ATTACHMENT_REFERENCED_TTL_MS = 72 * 60 * 60 * 1000;
15
+ /** Unreferenced uploads (uploaded but never sent) are cleaned much sooner. */
16
+ export const ATTACHMENT_ORPHAN_TTL_MS = 24 * 60 * 60 * 1000;
17
+ export function attachmentsRoot() {
18
+ return dshHomePath('agent-team', 'attachments', 'v1');
19
+ }
20
+ export function newAttachmentId() {
21
+ return randomUUID();
22
+ }
23
+ /** Strip path separators, control characters, and leading dots from one client-supplied name. */
24
+ export function sanitizeFileName(raw) {
25
+ const cleaned = raw.replaceAll(/[\\/\u0000-\u001f\u007f]/g, '').replaceAll(/^\.+/g, '').trim();
26
+ return cleaned === '' ? 'attachment' : cleaned.slice(0, 180);
27
+ }
28
+ /** Extension-derived media types for agent-supplied files; unknown types stay generic. */
29
+ const PATH_MEDIA_TYPES = {
30
+ '.png': 'image/png',
31
+ '.jpg': 'image/jpeg',
32
+ '.jpeg': 'image/jpeg',
33
+ '.gif': 'image/gif',
34
+ '.webp': 'image/webp',
35
+ '.bmp': 'image/bmp',
36
+ '.svg': 'image/svg+xml',
37
+ '.pdf': 'application/pdf',
38
+ '.txt': 'text/plain',
39
+ '.md': 'text/markdown',
40
+ '.json': 'application/json',
41
+ };
42
+ /** Best-effort media type from one path's extension, so images render as thumbnails. */
43
+ export function mediaTypeForPath(raw) {
44
+ return PATH_MEDIA_TYPES[extname(raw).toLowerCase()] ?? 'application/octet-stream';
45
+ }
46
+ /**
47
+ * Validate one agent-supplied attachment path before any cache write happens,
48
+ * so a rejection anywhere leaves the upload cache untouched.
49
+ */
50
+ export async function validatePathAttachment(raw) {
51
+ if (!isAbsolute(raw))
52
+ throw new Error(`attachment path '${raw}' must be absolute`);
53
+ const info = await stat(raw).catch(() => undefined);
54
+ if (info === undefined)
55
+ throw new Error(`attachment path '${raw}' does not exist`);
56
+ if (!info.isFile())
57
+ throw new Error(`attachment path '${raw}' is not a regular file`);
58
+ if (info.size === 0)
59
+ throw new Error(`attachment '${basename(raw)}' must not be empty`);
60
+ if (info.size > ATTACHMENT_MAX_BYTES)
61
+ throw new Error(`attachment '${basename(raw)}' exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
62
+ }
63
+ /** Copy one validated file into the cache as a fresh immutable entry. */
64
+ export async function copyPathAttachment(root, raw) {
65
+ const bytes = await readFile(raw);
66
+ const stored = await writeAttachment(root, newAttachmentId(), basename(raw), mediaTypeForPath(raw), bytes);
67
+ return { attachmentId: stored.attachmentId, name: stored.name, byteSize: stored.byteSize, mediaType: stored.mediaType };
68
+ }
69
+ function attachmentDir(root, attachmentId) {
70
+ return join(root, attachmentId);
71
+ }
72
+ /** Write one upload as an immutable payload plus its metadata sidecar. */
73
+ export async function writeAttachment(root, attachmentId, rawName, mediaType, bytes) {
74
+ const name = sanitizeFileName(rawName);
75
+ const dir = attachmentDir(root, attachmentId);
76
+ await mkdir(dir, { recursive: true });
77
+ await writeFile(join(dir, name), bytes);
78
+ const meta = { name, mediaType, uploadedAt: new Date().toISOString() };
79
+ await writeFile(join(dir, 'meta.json'), JSON.stringify(meta), 'utf8');
80
+ return { attachmentId, path: join(dir, name), name, byteSize: bytes.byteLength, mediaType };
81
+ }
82
+ /** Read one attachment back; `undefined` when the cache entry is gone (GC'd). */
83
+ export async function readAttachment(root, attachmentId) {
84
+ const dir = attachmentDir(root, attachmentId);
85
+ let metaRaw;
86
+ try {
87
+ metaRaw = await readFile(join(dir, 'meta.json'));
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ const meta = JSON.parse(metaRaw.toString('utf8'));
93
+ const entries = await readdir(dir);
94
+ const payload = entries.filter(entry => entry !== 'meta.json')[0];
95
+ if (payload === undefined)
96
+ return undefined;
97
+ const bytes = await readFile(join(dir, payload));
98
+ return { name: meta.name, mediaType: meta.mediaType, byteSize: bytes.byteLength, uploadedAt: meta.uploadedAt, bytes };
99
+ }
100
+ /** Whether an upload with this id still has bytes on disk. */
101
+ export async function attachmentExists(root, attachmentId) {
102
+ try {
103
+ await stat(attachmentDir(root, attachmentId));
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /** List every cache entry with its upload instant for the GC sweep. */
111
+ export async function scanAttachmentCache(root) {
112
+ let ids;
113
+ try {
114
+ ids = await readdir(root);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ const entries = [];
120
+ for (const id of ids) {
121
+ try {
122
+ const metaRaw = await readFile(join(root, id, 'meta.json'));
123
+ const meta = JSON.parse(metaRaw.toString('utf8'));
124
+ entries.push({ attachmentId: id, uploadedAt: Date.parse(meta.uploadedAt) });
125
+ }
126
+ catch {
127
+ // A half-written or foreign directory is not ours to judge; skip it.
128
+ }
129
+ }
130
+ return entries;
131
+ }
132
+ /** Remove one cache entry's bytes; missing entries already satisfy the sweep. */
133
+ export async function removeAttachment(root, attachmentId) {
134
+ await rm(attachmentDir(root, attachmentId), { recursive: true, force: true });
135
+ }
136
+ /**
137
+ * One GC pass: drop uploads older than the orphan TTL, and referenced ones
138
+ * older than the consumption-window TTL. Returns the ids removed so the
139
+ * service can log them.
140
+ */
141
+ export async function sweepAttachmentCache(root, referenced, now) {
142
+ const removed = [];
143
+ for (const entry of await scanAttachmentCache(root)) {
144
+ const age = now - entry.uploadedAt;
145
+ const ttl = referenced.has(entry.attachmentId) ? ATTACHMENT_REFERENCED_TTL_MS : ATTACHMENT_ORPHAN_TTL_MS;
146
+ if (age > ttl) {
147
+ await removeAttachment(root, entry.attachmentId);
148
+ removed.push(entry.attachmentId);
149
+ }
150
+ }
151
+ return removed;
152
+ }
153
+ /** Accept only well-formed `type/subtype` media types; anything else falls back to the generic binary type. */
154
+ export function sanitizeMediaType(raw) {
155
+ const candidate = (raw ?? '').trim().toLowerCase();
156
+ return /^[a-z0-9][-.\w]*\/[-.\w]+$/.test(candidate) ? candidate : 'application/octet-stream';
157
+ }
@@ -50,7 +50,9 @@ import { scopeOf } from '@deepseek-ai/dsh-scope';
50
50
  import { SessionId } from '@deepseek-ai/dsh-session';
51
51
  import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
52
52
  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";
53
54
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
55
+ import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_ATTEMPTS } from "./recovery.js";
54
56
  import { agentTeamDomainSpec } from "./spec.js";
55
57
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
56
58
  export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
@@ -86,11 +88,15 @@ let AgentTeam = (() => {
86
88
  let _createChannel_decorators;
87
89
  let _updateChannel_decorators;
88
90
  let _addMember_decorators;
91
+ let _recoverMember_decorators;
92
+ let _restartMember_decorators;
89
93
  let _updateMember_decorators;
90
94
  let _changeTask_decorators;
91
95
  let _joinChannel_decorators;
92
96
  let _removeChannelMember_decorators;
93
97
  let _sendMessage_decorators;
98
+ let _putAttachment_decorators;
99
+ let _getAttachment_decorators;
94
100
  let _reply_decorators;
95
101
  let _changeAttention_decorators;
96
102
  let _inbox_decorators;
@@ -106,11 +112,15 @@ let AgentTeam = (() => {
106
112
  _createChannel_decorators = [Remote('createChannel')];
107
113
  _updateChannel_decorators = [Remote('updateChannel')];
108
114
  _addMember_decorators = [Remote('addMember')];
115
+ _recoverMember_decorators = [Remote('recoverMember')];
116
+ _restartMember_decorators = [Remote('restartMember')];
109
117
  _updateMember_decorators = [Remote('updateMember')];
110
118
  _changeTask_decorators = [Remote('changeTask')];
111
119
  _joinChannel_decorators = [Remote('joinChannel')];
112
120
  _removeChannelMember_decorators = [Remote('removeChannelMember')];
113
121
  _sendMessage_decorators = [Remote('sendMessage')];
122
+ _putAttachment_decorators = [Remote('putAttachment')];
123
+ _getAttachment_decorators = [Remote('getAttachment')];
114
124
  _reply_decorators = [Remote('reply')];
115
125
  _changeAttention_decorators = [Remote('changeAttention')];
116
126
  _inbox_decorators = [Remote('inbox')];
@@ -123,11 +133,15 @@ let AgentTeam = (() => {
123
133
  __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
134
  __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
135
  __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
+ __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);
126
138
  __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
139
  __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
140
  __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
141
  __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
142
  __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);
143
+ __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);
144
+ __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
145
  __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
146
  __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
147
  __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);
@@ -151,7 +165,19 @@ let AgentTeam = (() => {
151
165
  handles = new Map();
152
166
  diagnostics = new Map();
153
167
  runtimeErrors = new Map();
168
+ /** Agent ids with a turn in flight; restarts must wait for the boundary. */
169
+ runningAgents = new Set();
154
170
  notifiedInbox = new Map();
171
+ attachmentGcTimer;
172
+ 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);
176
+ },
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`);
179
+ },
180
+ });
155
181
  lifecycleTail = Promise.resolve();
156
182
  accepting = true;
157
183
  changeVersion = 0;
@@ -165,11 +191,20 @@ let AgentTeam = (() => {
165
191
  const member = this.memberForAgent(agent);
166
192
  if (member === undefined)
167
193
  return;
168
- this.runtimeErrors.set(agent.id, error instanceof Error ? error.message : String(error));
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ this.runtimeErrors.set(agent.id, message);
196
+ const kind = classifyRecoverableError(message);
197
+ if (kind !== undefined)
198
+ this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable error (${kind}); automatic recovery scheduled`);
199
+ this.recovery.onError(member.memberId, message);
169
200
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
170
201
  });
171
202
  this.ctx.on('agent/status', ({ agent, status }) => {
172
203
  const member = this.memberForAgent(agent);
204
+ if (status === 'running')
205
+ this.runningAgents.add(agent.id);
206
+ else
207
+ this.runningAgents.delete(agent.id);
173
208
  if (status === 'running' && member !== undefined) {
174
209
  const recovered = this.runtimeErrors.delete(agent.id);
175
210
  if (recovered)
@@ -177,14 +212,23 @@ let AgentTeam = (() => {
177
212
  this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
178
213
  this.notifyMember(agent);
179
214
  }
215
+ // A turn that ends without an error closes any automatic recovery episode.
216
+ if (status === 'idle' && member !== undefined && !this.runtimeErrors.has(agent.id)) {
217
+ this.recovery.onCleanTurnEnd(member.memberId);
218
+ }
180
219
  });
181
220
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
182
221
  this.ctx.effect(() => async () => {
183
222
  this.accepting = false;
223
+ this.recovery.dispose();
224
+ if (this.attachmentGcTimer !== undefined)
225
+ clearInterval(this.attachmentGcTimer);
226
+ this.attachmentGcTimer = undefined;
184
227
  this.emitChanged();
185
228
  await this.lifecycleTail;
186
229
  await Promise.all([...this.handles.values()].map(handle => handle.dispose()));
187
230
  this.handles.clear();
231
+ this.runningAgents.clear();
188
232
  await domain.close();
189
233
  }, 'agentTeam.dispose');
190
234
  this.domain = domain;
@@ -193,6 +237,7 @@ let AgentTeam = (() => {
193
237
  const initialization = await ledger.initialize();
194
238
  if (initialization.committed)
195
239
  this.emitCommitted(initialization.value);
240
+ this.startAttachmentGc(ledger);
196
241
  // One metadata listing serves every Member restore; per-member list calls
197
242
  // would repeat the same I/O linearly during startup.
198
243
  const persistedSessions = new Set((await this.ctx.sessionPersistence.list()).map(header => header.id));
@@ -293,6 +338,7 @@ let AgentTeam = (() => {
293
338
  async addMember(request) {
294
339
  return this.enqueueLifecycle(async () => {
295
340
  const workspace = this.requireWorkspace(request.workspaceId);
341
+ await this.assertModelRoute(request.model);
296
342
  const memberId = `member:${randomUUID()}`;
297
343
  const member = Object.freeze({
298
344
  memberId,
@@ -341,6 +387,129 @@ let AgentTeam = (() => {
341
387
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
342
388
  });
343
389
  }
390
+ /**
391
+ * Operator nudge for an error-stopped Member: steer a continuation prompt
392
+ * into its live session. Runtime-only — no ledger operation, no suspend.
393
+ * Taking over manually also cancels any pending automatic recovery episode.
394
+ */
395
+ async recoverMember(request) {
396
+ this.requireAccepting();
397
+ const member = this.requireLedger().getMember(request.memberId);
398
+ if (member === undefined || member.workspaceId !== request.workspaceId)
399
+ throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
400
+ const storedError = this.storedErrorFor(member.memberId);
401
+ this.recovery.stopTracking(request.memberId);
402
+ const kind = classifyRecoverableError(storedError ?? '');
403
+ this.ctx.logger.info(`agent-team: operator asked member '${member.handle}' to resume`);
404
+ this.steerResume(member, this.resumeText(kind ?? 'unspecified failure', { operatorRequested: true }));
405
+ return Object.freeze({ status: this.memberStatus(member) });
406
+ }
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
+ /** Ledger handle for log lines; falls back to the raw id when unknown. */
449
+ memberLabel(memberId) {
450
+ return this.ledger?.getMember(memberId)?.handle ?? memberId;
451
+ }
452
+ /**
453
+ * Cache GC: uploads referenced by a Message survive 72h from upload so
454
+ * Member agents keep a consumption window; orphans (never sent) go after
455
+ * 24h. Runs once at startup and then daily — in-process only, because the
456
+ * cache is transient by design and rebuilds nothing across restarts.
457
+ */
458
+ startAttachmentGc(ledger) {
459
+ const sweep = async () => {
460
+ await sweepAttachmentCache(attachmentsRoot(), ledger.referencedAttachmentIds(), Date.now());
461
+ };
462
+ void sweep();
463
+ this.attachmentGcTimer = setInterval(() => { void sweep(); }, 24 * 60 * 60 * 1000);
464
+ this.attachmentGcTimer.unref?.();
465
+ }
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);
472
+ }
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;
486
+ }
487
+ /**
488
+ * Steer one continuation message into a Member's live session. Throws when
489
+ * no handle exists so the coordinator stops tracking; appends the inbox
490
+ * snapshot whenever there is anything new to read.
491
+ */
492
+ steerResume(member, text) {
493
+ const handle = this.handles.get(member.memberId);
494
+ if (handle === undefined)
495
+ throw new Error(`member '${member.handle}' has no active session`);
496
+ const notifications = this.requireLedger().notificationFacts(member.memberId, { workspaceId: member.workspaceId });
497
+ const body = notifications.length === 0 ? text : `${text}\n\n${this.notificationText(notifications)}`;
498
+ const hint = createUserMessage({
499
+ content: [{ type: 'text', text: body }],
500
+ source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Recovery: continue your interrupted work.' },
501
+ });
502
+ handle.agent.steer(hint);
503
+ }
504
+ /** Automatic-recovery injection; throwing tells the coordinator the target is gone. */
505
+ injectRecovery(memberId, attempt, kind) {
506
+ const handle = this.handles.get(memberId);
507
+ const agent = handle?.agent;
508
+ const member = agent !== undefined ? this.memberForAgent(agent) : undefined;
509
+ if (agent === undefined || member === undefined || member.state !== 'enabled')
510
+ throw new Error(`member '${memberId}' cannot be recovered automatically`);
511
+ this.steerResume(member, this.resumeText(kind, { attempt }));
512
+ }
344
513
  /**
345
514
  * Human edit of one Member's mutable facts. A model change restarts an
346
515
  * active Member: single-Session Members read their selection only at
@@ -349,6 +518,7 @@ let AgentTeam = (() => {
349
518
  */
350
519
  async updateMember(request) {
351
520
  return this.enqueueLifecycle(async () => {
521
+ await this.assertModelRoute(request.model);
352
522
  const previous = this.requireLedger().getMember(request.memberId);
353
523
  const result = await this.requireLedger().updateMember({ ...request, actor: agentTeamHumanActor() });
354
524
  if (result.committed)
@@ -413,15 +583,84 @@ let AgentTeam = (() => {
413
583
  async sendMessage(request) {
414
584
  this.requireAccepting();
415
585
  this.requireWorkspace(request.workspaceId);
416
- const result = await this.requireLedger().sendMessage({ ...request, actor: agentTeamHumanActor() });
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
+ });
417
592
  this.emitCommittedOutcome(result);
418
593
  return result.value;
419
594
  }
595
+ /**
596
+ * Resolve uploaded ids and agent-supplied absolute paths into one attachment
597
+ * metadata list. Paths are all validated before anything is copied, so one
598
+ * rejection leaves the cache untouched and the message uncommitted.
599
+ */
600
+ async resolveMessageAttachments(request) {
601
+ const fromPaths = [];
602
+ if (request.attachmentPaths !== undefined && request.attachmentPaths.length > 0) {
603
+ for (const absolutePath of request.attachmentPaths)
604
+ await validatePathAttachment(absolutePath);
605
+ for (const absolutePath of request.attachmentPaths)
606
+ fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath)));
607
+ }
608
+ return [...fromPaths, ...await this.prepareAttachments(request.attachments)];
609
+ }
610
+ /** Verify requested attachment ids against the cache. */
611
+ async prepareAttachments(requested) {
612
+ if (requested === undefined || requested.length === 0)
613
+ return [];
614
+ const metadata = [];
615
+ for (const attachmentId of requested) {
616
+ const stored = await readAttachment(attachmentsRoot(), attachmentId);
617
+ if (stored === undefined)
618
+ throw new Error(`attachment '${attachmentId}' is not in the upload cache`);
619
+ metadata.push(Object.freeze({ attachmentId, name: stored.name, byteSize: stored.byteSize, mediaType: stored.mediaType }));
620
+ }
621
+ return metadata;
622
+ }
623
+ /**
624
+ * Derive the stored body: one machine-facing `[attachment] <absolute path>`
625
+ * line per attachment appended to the member-facing text.
626
+ */
627
+ appendAttachmentLines(body, metadata) {
628
+ const trimmed = body.trim();
629
+ if (metadata.length === 0)
630
+ return trimmed;
631
+ const lines = metadata.map(attachment => `[attachment] ${dshHomePath('agent-team', 'attachments', 'v1', attachment.attachmentId, attachment.name)}`);
632
+ return `${trimmed}\n${lines.join('\n')}`;
633
+ }
634
+ /** Upload one composer attachment into the cache; bytes are immutable once written. */
635
+ async putAttachment(request) {
636
+ this.requireAccepting();
637
+ this.requireWorkspace(request.workspaceId);
638
+ const bytes = Buffer.from(request.bytesBase64, 'base64');
639
+ if (bytes.byteLength === 0)
640
+ throw new Error('attachment must not be empty');
641
+ if (bytes.byteLength > ATTACHMENT_MAX_BYTES)
642
+ throw new Error(`attachment exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
643
+ const mediaType = sanitizeMediaType(request.mediaType);
644
+ const attachmentId = newAttachmentId();
645
+ return Object.freeze(await writeAttachment(attachmentsRoot(), attachmentId, request.name, mediaType, bytes));
646
+ }
647
+ /** Read one cached attachment back for client display; gone entries throw and the UI degrades to a chip. */
648
+ async getAttachment(request) {
649
+ const stored = await readAttachment(attachmentsRoot(), request.attachmentId);
650
+ if (stored === undefined)
651
+ throw new Error(`attachment '${request.attachmentId}' is no longer cached`);
652
+ return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
653
+ }
420
654
  /** Human existing-Thread reply; unread and revision conflicts are business outcomes. */
421
655
  async reply(request) {
422
656
  this.requireAccepting();
423
657
  this.requireWorkspace(request.workspaceId);
424
- const result = await this.requireLedger().reply({ ...request, actor: agentTeamHumanActor() });
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
+ });
425
664
  this.emitCommittedOutcome(result);
426
665
  return result.value;
427
666
  }
@@ -468,7 +707,12 @@ let AgentTeam = (() => {
468
707
  this.requireAccepting();
469
708
  const actor = this.memberActor(agent);
470
709
  this.requireAgentWorkspace(actor, request.workspaceId);
471
- const result = await this.requireLedger().sendMessage({ ...request, actor });
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
+ });
472
716
  this.emitCommittedOutcome(result);
473
717
  return result.value;
474
718
  }
@@ -477,7 +721,12 @@ let AgentTeam = (() => {
477
721
  this.requireAccepting();
478
722
  const actor = this.memberActor(agent);
479
723
  this.requireAgentWorkspace(actor, request.workspaceId);
480
- const result = await this.requireLedger().reply({ ...request, actor });
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
+ });
481
730
  this.emitCommittedOutcome(result);
482
731
  return result.value;
483
732
  }
@@ -565,6 +814,26 @@ let AgentTeam = (() => {
565
814
  if (this.requireLedger().getMember(actor.memberId)?.workspaceId !== workspaceId)
566
815
  throw new Error('Member cannot mutate another Workspace');
567
816
  }
817
+ /**
818
+ * Validate a pinned model route's reasoning effort against the adapter's own
819
+ * metadata when the LLM service is reachable; unknown routes defer to the
820
+ * LLM layer's runtime check at call time.
821
+ */
822
+ async assertModelRoute(model) {
823
+ if (model === undefined || model.reasoningEffort === undefined)
824
+ return;
825
+ try {
826
+ const resolved = await this.ctx.llm.resolveModelInfo(model.provider, model.model);
827
+ const efforts = resolved.reasoning?.efforts ?? [];
828
+ if (efforts.length > 0 && !efforts.some(effort => effort.id === model.reasoningEffort)) {
829
+ throw new Error(`reasoning effort '${model.reasoningEffort}' is not supported by ${model.provider}/${model.model}`);
830
+ }
831
+ }
832
+ catch (error) {
833
+ if (error instanceof Error && error.message.includes('is not supported by'))
834
+ throw error;
835
+ }
836
+ }
568
837
  async activateMember(member, knownWorkspacePath, knownSessions) {
569
838
  if (this.handles.has(member.memberId))
570
839
  return;