dsh-team-limits 1.0.0 → 1.3.4

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ ## 1.3.4
4
+
5
+ - The panel description no longer explains where the values are stored. It names the four limits
6
+ and stops; the page itself shows how to edit them.
7
+
8
+ ## 1.3.3
9
+
10
+ - Dropped the "nothing else is touched" clause from the panel description in both languages. The
11
+ description now states what the page does and stops there.
12
+
13
+ ## 1.3.2
14
+
15
+ - The panel description now says what the bundle does (adjust the Agent Team limits from the plugin
16
+ page, written onto the agent-team row) instead of quoting one fixed set of numbers, which had
17
+ been stale since 1.2.0 turned the limits into editable fields.
18
+ - The README follows the same wording: it points at Settings → Plugins → Installed → Team limits
19
+ and no longer claims a Save button or a standalone Settings page.
20
+
21
+ ## 1.3.1
22
+
23
+ - The form now lives on the plugin's own page: opening "Team limits" in Plugins → Installed shows
24
+ the four fields right below the bundle description, through the native `plugins.bundle.config`
25
+ slot keyed by the package name. The extra tab in the Plugins section is gone.
26
+ - The compact `summary` view of the same slot renders one line (members · tasks · bytes · ms)
27
+ instead of the whole form.
28
+
29
+ ## 1.3.0
30
+
31
+ - The page now lives where the rest of a plugin's UI lives: **Settings → Plugins → Team limits**,
32
+ as a tab of the Plugins section, instead of a standalone row in the Settings sidebar.
33
+ - The page was rebuilt to match the shipped settings pages: a title and one-line introduction,
34
+ section headings, two columns, and per-field "Overridden" markers with a "Restore default"
35
+ action. Edits are applied as you type (debounced), so there is no Save button.
36
+ - The four numbers are grouped as "Runtime limits" (members, tasks) and "Messages and teardown"
37
+ (message bytes, disposal timeout).
38
+
39
+ ## 1.2.0
40
+
41
+ - New: a real settings page, **Settings → Team limits**, with four number fields, Save and
42
+ Restore defaults. Values are written onto the shipped `agent-team` row through the host's
43
+ config editor instead of being frozen in the bundle patch.
44
+ - The bundle patch no longer overrides `agent-team`; it only mounts the row, so a DSH upgrade
45
+ that changes the shipped defaults no longer fights with stale numbers in this file.
46
+ - Added the browser half (`client.js`, `dsh.client`) and a pinned `maxMembers: 32` starting
47
+ point; the page still lives in the harness Settings shell, so nothing renders in
48
+ terminal-only profiles.
49
+
50
+ ## 1.0.1
51
+
52
+ - Added a real entry artifact (`index.js`) and `main`/`exports`, fixing the plugin-manager
53
+ validation error "declared entry artifact is missing".
54
+ - The bundle now inserts one small component row (`team-limits`) that registers nothing, so the
55
+ plugin panel lists the bundle under "components" instead of reporting that it contains none.
56
+ - Added localized title and description (English / 简体中文) and a purpose-drawn icon: the limits
57
+ are documented in the panel instead of only in this README.
58
+
59
+ ## 1.0.0
60
+
61
+ - First release: overrides the shipped `agent-team` row with `maxMembers: 32`, `maxTasks: 512`,
62
+ `maxMessageBytes: 131072`, `disposalTimeoutMs: 5000`.
package/README.md CHANGED
@@ -2,34 +2,36 @@
2
2
 
3
3
  **English** · [简体中文](#简体中文)
4
4
 
5
- Optional companion bundle for [dsh-subagent-control](https://github.com/SpaceRelay/dsh-subagent-control): raises the Agent Team limits that ship with DeepSeek Harness.
5
+ Optional companion bundle for [dsh-subagent-control](https://github.com/SpaceRelay/dsh-subagent-control): it adds editable **Team limits** fields to the plugin's own page (Settings → Plugins → Installed → Team limits), so the Agent Team limits are number fields you can change and reset — instead of values frozen in a patch file.
6
6
 
7
7
  ## English
8
8
 
9
- The Agent Team plugin ships with these limits:
9
+ ### What it does
10
10
 
11
- | Setting | Shipped default | This bundle |
11
+ DeepSeek Harness ships the Agent Team plugin with these limits:
12
+
13
+ | Setting | Shipped default | This page starts at |
12
14
  | --- | --- | --- |
13
15
  | `maxMembers` | 16 | **32** |
14
16
  | `maxTasks` | 256 | **512** |
15
17
  | `maxMessageBytes` | 65536 | **131072** |
16
- | `disposalTimeoutMs` | 5000 | 5000 (unchanged) |
18
+ | `disposalTimeoutMs` | 5000 | 5000 |
17
19
 
18
- This is not a plugin with tools. It is a bundle whose patch targets the existing `agent-team` row and replaces its `config`. Nothing changes until you install it, which is the point: raising somebody else's limits is a deliberate choice, not a default.
20
+ The page has four number fields. Edits are written onto the shipped `agent-team` row through the host's config editor as you type (debounced, no Save button), and each field offers **Restore default** once it differs from the shipped value. The config editor persists the values in the profile patch layer and applies them on the normal Loader path. The bundle's own patch therefore holds no numbers, so a DSH upgrade that changes the shipped defaults does not silently resurrect old values.
19
21
 
20
22
  ### Before you install
21
23
 
22
- - **A patch replaces `config` wholesale, never deep-merges.** The four fields above are restated in this bundle for that reason; if the host adds a fifth field, this bundle will not supply it and that field falls back to its default.
23
- - **Restart DSH after installing.** The row is read at startup.
24
- - **Raising limits is not free.** More members means more concurrent model sessions; each one costs tokens and a session directory. Raise only what you actually need.
25
- - **Uninstall restores the shipped defaults**; nothing is written anywhere else.
24
+ - **Restart DSH after installing**, then open Settings → Plugins → Installed → Team limits; the fields sit right below the bundle description.
25
+ - **Values live in the profile patch layer.** Uninstalling the bundle does not roll them back; set the fields back to the shipped defaults (or remove the `agent-team` override in the profile patch) if you want the stock numbers again.
26
+ - **Raising limits is not free.** More members means more concurrent model sessions; each one costs tokens and a session directory. Raise only what you need.
27
+ - The client page needs the harness browser bundle; nothing is rendered in terminal-only profiles, but the limits still apply.
26
28
 
27
29
  ### Install
28
30
 
29
31
  Through the plugin panel / market, or by asking the agent:
30
32
 
31
33
  ```
32
- plugin_manager install_bundle target "dsh-team-limits@1.0.0"
34
+ plugin_manager install_bundle target "dsh-team-limits@1.3.2"
33
35
  ```
34
36
 
35
37
  ### License
@@ -40,32 +42,34 @@ MIT — see [LICENSE](./LICENSE). Issues: <https://github.com/SpaceRelay/dsh-sub
40
42
 
41
43
  ## 简体中文
42
44
 
43
- 这是 [dsh-subagent-control](https://github.com/SpaceRelay/dsh-subagent-control) 的可选同伴包,用来提高 DeepSeek Harness 自带 Agent Team 的上限。
45
+ 这是 [dsh-subagent-control](https://github.com/SpaceRelay/dsh-subagent-control) 的可选同伴包:在**插件自己的页面**上(设置 → 插件 → 已安装 → 团队限额)加一组可编辑的「团队限额」字段,把 Agent Team 的上限变成能直接填、能恢复默认的数字输入框,而不是写死在补丁文件里的值。
46
+
47
+ ### 它做什么
44
48
 
45
- Agent Team 插件出厂限制如下:
49
+ Agent Team 出厂限制如下:
46
50
 
47
- | 配置 | 出厂默认 | 本包 |
51
+ | 配置 | 出厂默认 | 本页面初始值 |
48
52
  | --- | --- | --- |
49
53
  | `maxMembers` | 16 | **32** |
50
54
  | `maxTasks` | 256 | **512** |
51
55
  | `maxMessageBytes` | 65536 | **131072** |
52
- | `disposalTimeoutMs` | 5000 | 5000(不变) |
56
+ | `disposalTimeoutMs` | 5000 | 5000 |
53
57
 
54
- 它不是一个带工具的插件,而是一个**只做覆盖的 bundle**:补丁指向现有的 `agent-team` 行,整体替换它的 `config`。不安装就完全不生效——把别人的上限改高应该是主动选择,不是默认行为。
58
+ 页面有四个数字输入框。改动边输边写(防抖,没有保存按钮),每个字段一旦偏离出厂值就会出现**恢复默认**。宿主侧的配置编辑器会把这些值写到自带的 `agent-team` 行上,持久化进 profile 的补丁层,并按正常加载流程生效。因此本包自己的补丁里不再存数值:DSH 升级改变了出厂默认,也不会悄悄把旧数字带回来。
55
59
 
56
60
  ### 安装前要知道
57
61
 
58
- - **补丁是整体替换 `config`,不是深合并。** 上面四个字段因此必须写全;如果宿主以后新增第五个字段,本包不会提供它,那个字段会回到默认值。
59
- - **安装后需要重启 DSH**,这一行在启动时读取。
60
- - **提高上限不是免费的。** 队友更多意味着更多并发的模型会话,每个都消耗 token 与一个会话目录;按实际需要调,不要一次拉满。
61
- - **卸载即恢复出厂默认**,不写任何其它位置。
62
+ - **安装后重启 DSH**,然后打开 设置 → 插件 → 已安装 → 团队限额;输入框就在插件描述下面。
63
+ - **数值存在 profile 的补丁层里。** 卸载本包不会把它们撤回;要恢复出厂值,请在页面上点「恢复默认」,或删除 profile 补丁里 `agent-team` 那条覆盖。
64
+ - **提高上限不是免费的。** 队友更多意味着更多并发的模型会话,每个都消耗 token 与一个会话目录;按实际需要调。
65
+ - 这个页面依赖 Harness 的浏览器端;纯终端 profile 不渲染页面,但限额照样生效。
62
66
 
63
67
  ### 安装
64
68
 
65
69
  在插件面板 / 市场中安装,或者让智能体执行:
66
70
 
67
71
  ```
68
- plugin_manager install_bundle target "dsh-team-limits@1.0.0"
72
+ plugin_manager install_bundle target "dsh-team-limits@1.3.2"
69
73
  ```
70
74
 
71
75
  ### 许可
package/client.js ADDED
@@ -0,0 +1,385 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-team-limits',
3
+ factory: (require) => {
4
+ var module = { exports: {} }
5
+ var exports = module.exports
6
+
7
+ var React = require('react')
8
+ var h = React.createElement
9
+
10
+ var NS = 'team-limits'
11
+
12
+ var DEFAULTS = {
13
+ maxMembers: 32,
14
+ maxTasks: 512,
15
+ maxMessageBytes: 131072,
16
+ disposalTimeoutMs: 5000,
17
+ }
18
+
19
+ var SECTIONS = [
20
+ {
21
+ id: 'runtime',
22
+ zh: '运行限制',
23
+ en: 'Runtime limits',
24
+ zhNote: '一个团队同时能容纳的成员数与任务板容量。',
25
+ enNote: 'How many members a team holds and how many tasks its board keeps.',
26
+ fields: [
27
+ {
28
+ key: 'maxMembers',
29
+ zh: '团队人数上限',
30
+ en: 'Maximum team members',
31
+ zhHelp: '含你(Lead)在内的成员总数上限。',
32
+ enHelp: 'Members per team, the Lead included.',
33
+ },
34
+ {
35
+ key: 'maxTasks',
36
+ zh: '任务数上限',
37
+ en: 'Maximum tasks',
38
+ zhHelp: '共享任务板上同时保留的任务数。',
39
+ enHelp: 'Tasks the shared board keeps at once.',
40
+ },
41
+ ],
42
+ },
43
+ {
44
+ id: 'messaging',
45
+ zh: '消息与清理',
46
+ en: 'Messages and teardown',
47
+ zhNote: '单条消息的体积上限,以及成员停止时的等待时间。',
48
+ enNote: 'Upper bound for one message, and how long teardown waits.',
49
+ fields: [
50
+ {
51
+ key: 'maxMessageBytes',
52
+ zh: '单条消息上限(字节)',
53
+ en: 'Maximum message size (bytes)',
54
+ zhHelp: '131072 字节 = 128 KB。',
55
+ enHelp: '131072 bytes = 128 KB.',
56
+ },
57
+ {
58
+ key: 'disposalTimeoutMs',
59
+ zh: '清理超时(毫秒)',
60
+ en: 'Disposal timeout (ms)',
61
+ zhHelp: '成员停止时最多等待多久再强制结束。',
62
+ enHelp: 'How long teardown waits before forcing the stop.',
63
+ },
64
+ ],
65
+ },
66
+ ]
67
+
68
+ function isZh() {
69
+ try {
70
+ return (navigator.language || '').toLowerCase().indexOf('zh') === 0
71
+ } catch (error) {
72
+ return false
73
+ }
74
+ }
75
+
76
+ var COPY = {
77
+ title: ['团队限额', 'Team limits'],
78
+ intro: [
79
+ '设置 Agent Team 的成员数、任务数、消息大小与清理超时。改动会写入 agent-team 行,按正常加载流程生效。',
80
+ 'Team size, task count, message size and teardown timeout for Agent Team. Changes are written onto the agent-team row and applied on the normal load path.',
81
+ ],
82
+ overridden: ['已覆盖', 'Overridden'],
83
+ reset: ['恢复默认', 'Restore default'],
84
+ loading: ['正在读取配置…', 'Loading configuration…'],
85
+ unavailable: [
86
+ '当前连接不支持写入设置,这一页只能查看。',
87
+ 'This connection cannot write settings, so this page is read-only.',
88
+ ],
89
+ saveFailed: ['保存失败:', 'Save failed: '],
90
+ note: [
91
+ '数值保存在当前 profile 的补丁层;卸载本插件不会自动撤回。',
92
+ 'Values persist in this profile patch layer; uninstalling the plugin does not roll them back.',
93
+ ],
94
+ }
95
+
96
+ function t(key) {
97
+ var pair = COPY[key]
98
+ if (pair === undefined) return key
99
+ return isZh() ? pair[0] : pair[1]
100
+ }
101
+
102
+ var CSS = [
103
+ '.dtl { display: flex; flex-direction: column; gap: 26px; max-width: 820px; padding: 4px 2px 8px; }',
104
+ '.dtl-head { display: flex; flex-direction: column; gap: 6px; }',
105
+ '.dtl-title { font-size: 22px; font-weight: 650; letter-spacing: .01em; margin: 0; line-height: 1.3; }',
106
+ '.dtl-sub { font-size: 13px; opacity: .62; line-height: 1.6; margin: 0; max-width: 64ch; }',
107
+ '.dtl-group { display: flex; flex-direction: column; gap: 14px; }',
108
+ '.dtl-group-title { font-size: 14.5px; font-weight: 600; margin: 0; line-height: 1.4; }',
109
+ '.dtl-group-note { font-size: 12px; opacity: .55; margin: -8px 0 0; line-height: 1.6; }',
110
+ '.dtl-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px; }',
111
+ '@media (max-width: 720px) { .dtl-grid { grid-template-columns: minmax(0, 1fr); } }',
112
+ '.dtl-cell { display: flex; flex-direction: column; gap: 8px; min-width: 0; }',
113
+ '.dtl-labelrow { display: flex; align-items: center; gap: 8px; min-height: 20px; }',
114
+ '.dtl-label { font-size: 13px; font-weight: 500; }',
115
+ '.dtl-chip { font-size: 11.5px; line-height: 18px; padding: 0 8px; border-radius: 999px; background: rgba(128,128,128,.14); opacity: .8; }',
116
+ '.dtl-spacer { flex: 1 1 auto; }',
117
+ '.dtl-reset { font-size: 12.5px; color: inherit; opacity: .55; background: none; border: none; padding: 0; cursor: pointer; }',
118
+ '.dtl-reset:hover:not(:disabled) { opacity: .85; text-decoration: underline; }',
119
+ '.dtl-reset:disabled { opacity: .25; cursor: default; }',
120
+ '.dtl-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; font-size: 13.5px; font-variant-numeric: tabular-nums; border-radius: 9px; border: 1px solid rgba(128,128,128,.36); background: transparent; color: inherit; outline: none; }',
121
+ '.dtl-input:focus { border-color: rgba(76,139,245,.85); }',
122
+ '.dtl-input:disabled { opacity: .45; }',
123
+ '.dtl-help { font-size: 12px; opacity: .5; line-height: 1.5; }',
124
+ '.dtl-foot { font-size: 12px; opacity: .48; line-height: 1.6; margin: 0; max-width: 64ch; }',
125
+ '.dtl-error { font-size: 12.5px; color: #d9534f; }',
126
+ '.dtl-summary { font-size: 13px; opacity: .78; font-variant-numeric: tabular-nums; }',
127
+ ].join('\n')
128
+
129
+ function numberOr(value, fallback) {
130
+ var parsed = typeof value === 'number' ? value : Number(value)
131
+ return Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : fallback
132
+ }
133
+
134
+ function valuesOf(source) {
135
+ var result = {}
136
+ for (var index = 0; index < SECTIONS.length; index += 1) {
137
+ var fields = SECTIONS[index].fields
138
+ for (var inner = 0; inner < fields.length; inner += 1) {
139
+ var key = fields[inner].key
140
+ result[key] = numberOr(source === null || source === undefined ? undefined : source[key], DEFAULTS[key])
141
+ }
142
+ }
143
+ return result
144
+ }
145
+
146
+ function Field(props) {
147
+ var field = props.field
148
+ var overridden = props.value !== DEFAULTS[field.key]
149
+ return h(
150
+ 'div',
151
+ { className: 'dtl-cell' },
152
+ h(
153
+ 'div',
154
+ { className: 'dtl-labelrow' },
155
+ h('label', { className: 'dtl-label', htmlFor: 'dtl-' + field.key }, isZh() ? field.zh : field.en),
156
+ overridden ? h('span', { className: 'dtl-chip' }, t('overridden')) : null,
157
+ h('span', { className: 'dtl-spacer' }),
158
+ overridden
159
+ ? h(
160
+ 'button',
161
+ {
162
+ type: 'button',
163
+ className: 'dtl-reset',
164
+ disabled: props.disabled,
165
+ onClick: function () {
166
+ props.onReset(field.key)
167
+ },
168
+ },
169
+ t('reset'),
170
+ )
171
+ : null,
172
+ ),
173
+ h('input', {
174
+ id: 'dtl-' + field.key,
175
+ className: 'dtl-input',
176
+ type: 'number',
177
+ min: 1,
178
+ step: 1,
179
+ inputMode: 'numeric',
180
+ disabled: props.disabled,
181
+ value: String(props.value),
182
+ onChange: function (event) {
183
+ props.onInput(field.key, event.target.value)
184
+ },
185
+ }),
186
+ h('span', { className: 'dtl-help' }, isZh() ? field.zhHelp : field.enHelp),
187
+ )
188
+ }
189
+
190
+ function TeamLimitsPage(props) {
191
+ var form = props.configForm || props.form
192
+ var summary = props.view === 'summary'
193
+
194
+ if (!form) {
195
+ return h('p', { className: 'dtl-foot' }, t('loading'))
196
+ }
197
+
198
+ var snapshotPair = React.useState(function () {
199
+ return form.getSnapshot()
200
+ })
201
+ var snapshot = snapshotPair[0]
202
+ var setSnapshot = snapshotPair[1]
203
+
204
+ var draftPair = React.useState(function () {
205
+ return valuesOf(snapshot.value)
206
+ })
207
+ var draft = draftPair[0]
208
+ var setDraft = draftPair[1]
209
+
210
+ var errorPair = React.useState('')
211
+ var error = errorPair[0]
212
+ var setError = errorPair[1]
213
+
214
+ var pendingPair = React.useState({})
215
+ var pending = pendingPair[0]
216
+ var setPending = pendingPair[1]
217
+
218
+ React.useEffect(function () {
219
+ var off = form.subscribe(function () {
220
+ setSnapshot(form.getSnapshot())
221
+ })
222
+ setSnapshot(form.getSnapshot())
223
+ return off
224
+ }, [form])
225
+
226
+ React.useEffect(function () {
227
+ var next = valuesOf(snapshot.value)
228
+ var keys = Object.keys(pending)
229
+ setDraft(function (previous) {
230
+ var merged = {}
231
+ for (var name in previous) merged[name] = previous[name]
232
+ for (var name in next) {
233
+ if (keys.indexOf(name) < 0) merged[name] = next[name]
234
+ }
235
+ return merged
236
+ })
237
+ // eslint-disable-next-line react-hooks/exhaustive-deps
238
+ }, [snapshot.value])
239
+
240
+ React.useEffect(function () {
241
+ var keys = Object.keys(pending)
242
+ if (keys.length === 0) return undefined
243
+ var timer = setTimeout(function () {
244
+ var chain = Promise.resolve()
245
+ keys.forEach(function (key) {
246
+ chain = chain.then(function () {
247
+ return form.set(key, pending[key])
248
+ })
249
+ })
250
+ chain
251
+ .then(function () {
252
+ setPending({})
253
+ })
254
+ .catch(function (caught) {
255
+ setPending({})
256
+ setError(t('saveFailed') + String((caught && caught.message) || caught))
257
+ })
258
+ }, 700)
259
+ return function () {
260
+ clearTimeout(timer)
261
+ }
262
+ }, [pending, form])
263
+
264
+ var ready = snapshot.status === 'ready'
265
+ var disabled = !ready
266
+
267
+ function write(key, value) {
268
+ setError('')
269
+ setPending(function (previous) {
270
+ var next = {}
271
+ for (var name in previous) next[name] = previous[name]
272
+ next[key] = value
273
+ return next
274
+ })
275
+ }
276
+
277
+ function onInput(key, raw) {
278
+ var parsed = Number(raw)
279
+ var safe = Number.isFinite(parsed) ? Math.max(1, Math.floor(parsed)) : DEFAULTS[key]
280
+ setDraft(function (previous) {
281
+ var next = {}
282
+ for (var name in previous) next[name] = previous[name]
283
+ next[key] = safe
284
+ return next
285
+ })
286
+ if (raw === '') return
287
+ write(key, safe)
288
+ }
289
+
290
+ function onReset(key) {
291
+ setDraft(function (previous) {
292
+ var next = {}
293
+ for (var name in previous) next[name] = previous[name]
294
+ next[key] = DEFAULTS[key]
295
+ return next
296
+ })
297
+ write(key, DEFAULTS[key])
298
+ }
299
+
300
+ if (summary) {
301
+ return h(
302
+ 'span',
303
+ { className: 'dtl-summary' },
304
+ [
305
+ draft.maxMembers + (isZh() ? ' 成员' : ' members'),
306
+ draft.maxTasks + (isZh() ? ' 任务' : ' tasks'),
307
+ draft.maxMessageBytes + (isZh() ? ' 字节/条' : ' bytes/msg'),
308
+ draft.disposalTimeoutMs + ' ms',
309
+ ].join(' · '),
310
+ )
311
+ }
312
+
313
+ var groups = SECTIONS.map(function (section) {
314
+ return h(
315
+ 'section',
316
+ { className: 'dtl-group', key: section.id },
317
+ h('h3', { className: 'dtl-group-title' }, isZh() ? section.zh : section.en),
318
+ h('p', { className: 'dtl-group-note' }, isZh() ? section.zhNote : section.enNote),
319
+ h(
320
+ 'div',
321
+ { className: 'dtl-grid' },
322
+ section.fields.map(function (field) {
323
+ return h(Field, {
324
+ key: field.key,
325
+ field: field,
326
+ value: draft[field.key],
327
+ disabled: disabled,
328
+ onInput: onInput,
329
+ onReset: onReset,
330
+ })
331
+ }),
332
+ ),
333
+ )
334
+ })
335
+
336
+ return h(
337
+ 'div',
338
+ { className: 'dtl' },
339
+ h('style', null, CSS),
340
+ ready ? null : h('p', { className: 'dtl-foot' }, t(snapshot.status === 'unavailable' ? 'unavailable' : 'loading')),
341
+ groups,
342
+ error ? h('p', { className: 'dtl-error' }, error) : null,
343
+ h('p', { className: 'dtl-foot' }, t('note')),
344
+ )
345
+ }
346
+
347
+ function apply(ctx) {
348
+ try {
349
+ var forms = ctx.configForms
350
+ var form = forms ? forms.get(NS) : null
351
+ if (!form) {
352
+ console.error('[team-limits] configForms service is unavailable; no settings page is registered')
353
+ return
354
+ }
355
+ ctx.effect(
356
+ function () {
357
+ return forms.whileServed([NS], function () {
358
+ return ctx.slots.inject('plugins.bundle.config', function () {
359
+ return ctx.slots.register(
360
+ {
361
+ name: 'plugins.bundle.config',
362
+ key: 'dsh-team-limits',
363
+ inject: function () {
364
+ return { configForm: form }
365
+ },
366
+ },
367
+ TeamLimitsPage,
368
+ )
369
+ })
370
+ })
371
+ },
372
+ 'team-limits: bundle config',
373
+ )
374
+ } catch (error) {
375
+ console.error('[team-limits] client half failed to mount: ' + String((error && error.message) || error))
376
+ }
377
+ }
378
+
379
+ exports.name = 'dsh-team-limits'
380
+ exports.inject = ['slots', 'configForms']
381
+ exports.apply = apply
382
+
383
+ return module.exports
384
+ },
385
+ })
package/cordis.patch.yml CHANGED
@@ -1,15 +1,11 @@
1
- # Overrides the shipped Agent Team row. A patch that names an existing id
2
- # replaces that row's whole `config`, so every field is restated here; keep them
3
- # in sync with the host's agent-team schema when DSH changes.
1
+ # A settings-page bundle. The inserted row is the host half: it declares the
2
+ # four limits as Config and pushes the chosen values onto the shipped
3
+ # `agent-team` row through the config editor. The client half (dsh.client)
4
+ # renders the page under Settings → Team limits.
4
5
  #
5
- # default -> this bundle
6
- # maxMembers 16 -> 32
7
- # maxTasks 256 -> 512
8
- # maxMessageBytes 65536 -> 131072
9
- # disposalTimeoutMs 5000 -> 5000 (unchanged)
10
- - id: agent-team
11
- config:
12
- maxMembers: 32
13
- maxTasks: 512
14
- maxMessageBytes: 131072
15
- disposalTimeoutMs: 5000
6
+ # The limits are no longer frozen here: everything the page saves is written
7
+ # into the profile patch layer by the config editor, so this file stays free of
8
+ # values and survives a DSH upgrade that changes the agent-team defaults.
9
+ - insert:
10
+ - id: team-limits
11
+ name: 'dsh-team-limits'
package/icon.svg ADDED
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Team limits raised">
2
+ <title>Team limits</title>
3
+ <rect width="64" height="64" rx="14" fill="#1f6feb"/>
4
+ <g fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
5
+ <path d="M18 33h28"/>
6
+ <path d="M32 26V13"/>
7
+ <path d="M25 20l7-7 7 7"/>
8
+ </g>
9
+ <g fill="#ffffff">
10
+ <circle cx="21" cy="46" r="4"/>
11
+ <circle cx="32" cy="46" r="4"/>
12
+ <circle cx="43" cy="46" r="4"/>
13
+ </g>
14
+ </svg>
package/index.js ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * dsh-team-limits — Host half.
3
+ *
4
+ * The four Agent Team limits are this plugin's Config, so the browser half can
5
+ * render them as a real settings page. `apply` pushes the chosen values onto
6
+ * the shipped `agent-team` row through the config editor, which persists them
7
+ * in the profile patch layer and applies them on the normal Loader path.
8
+ *
9
+ * The editor refuses to run inside an existing HMR transaction ("HMR
10
+ * transactions cannot be nested"). Our apply() runs inside the transaction
11
+ * that mounted this plugin, and every timer created there inherits that
12
+ * context through AsyncLocalStorage, so a plain setTimeout would still be
13
+ * nested. `escapeTransaction` runs the push through the HMR service's own
14
+ * AsyncLocalStorage.exit, which is exactly the escape hatch the service
15
+ * itself uses for its watchers.
16
+ */
17
+
18
+ export const name = 'dsh-team-limits'
19
+
20
+ export const inject = ['configEditor']
21
+
22
+ const DEFAULTS = {
23
+ maxMembers: 32,
24
+ maxTasks: 512,
25
+ maxMessageBytes: 131072,
26
+ disposalTimeoutMs: 5000,
27
+ }
28
+
29
+ const RETRY_DELAYS = [1300, 4000, 8000]
30
+
31
+ const DEBUG = 'C:/Users/Administrator/.dsh/team-limits-debug.log'
32
+
33
+ async function trace(message) {
34
+ try {
35
+ const { appendFileSync } = await import('node:fs')
36
+ appendFileSync(DEBUG, new Date().toISOString() + ' ' + message + '\n')
37
+ } catch {
38
+ /* diagnostics must never break the plugin */
39
+ }
40
+ }
41
+
42
+ let Schema
43
+ try {
44
+ const loaded = await import('@deepseek-ai/schemastery')
45
+ Schema = loaded.default ?? loaded.Schema
46
+ } catch {
47
+ Schema = undefined
48
+ }
49
+
50
+ export const Config =
51
+ Schema === undefined
52
+ ? undefined
53
+ : Schema.object({
54
+ maxMembers: Schema.natural()
55
+ .min(1)
56
+ .volatile()
57
+ .default(DEFAULTS.maxMembers)
58
+ .description('Maximum number of teammates in one team.'),
59
+ maxTasks: Schema.natural()
60
+ .min(1)
61
+ .volatile()
62
+ .default(DEFAULTS.maxTasks)
63
+ .description('Maximum number of tasks the shared task board keeps.'),
64
+ maxMessageBytes: Schema.natural()
65
+ .min(1)
66
+ .volatile()
67
+ .default(DEFAULTS.maxMessageBytes)
68
+ .description('Maximum size of a single team message, in bytes (131072 = 128 KB).'),
69
+ disposalTimeoutMs: Schema.natural()
70
+ .min(1)
71
+ .volatile()
72
+ .default(DEFAULTS.disposalTimeoutMs)
73
+ .description('How long teardown waits for a member before giving up, in milliseconds.'),
74
+ }).description('Limits applied to the shipped Agent Team row.')
75
+
76
+ function normalize(config) {
77
+ const source = config ?? {}
78
+ const result = {}
79
+ for (const key of Object.keys(DEFAULTS)) {
80
+ const value = source[key]
81
+ result[key] =
82
+ typeof value === 'number' && Number.isFinite(value) && value >= 1
83
+ ? Math.floor(value)
84
+ : DEFAULTS[key]
85
+ }
86
+ return result
87
+ }
88
+
89
+ function entryId(entry) {
90
+ if (entry === null || entry === undefined) return ''
91
+ if (typeof entry === 'string') return entry
92
+ const candidates = [entry.options?.id, entry.id, entry.patch?.id, entry.entry?.id]
93
+ for (const candidate of candidates) {
94
+ if (typeof candidate === 'string' && candidate.length > 0) return candidate
95
+ }
96
+ return ''
97
+ }
98
+
99
+ function alreadyApplied(override, desired) {
100
+ if (override === null || override === undefined) return false
101
+ return Object.keys(desired).every((key) => override[key] === desired[key])
102
+ }
103
+
104
+ /**
105
+ * Run `work` outside any HMR transaction. The service keeps its flag in an
106
+ * AsyncLocalStorage; ours was inherited from the transaction that mounted this
107
+ * plugin, so only its own `exit` can clear it for a later asynchronous task.
108
+ */
109
+ function escapeTransaction(ctx, work) {
110
+ let executing
111
+ try {
112
+ const hmr = ctx.get('hmr')
113
+ executing = hmr === undefined ? undefined : hmr.executing
114
+ } catch {
115
+ executing = undefined
116
+ }
117
+ if (executing !== undefined && typeof executing.exit === 'function') {
118
+ return executing.exit(work)
119
+ }
120
+ return work()
121
+ }
122
+
123
+ /** @returns {Promise<boolean>} true when another attempt is worth making. */
124
+ async function push(ctx, desired, attempt) {
125
+ return escapeTransaction(ctx, async () => {
126
+ try {
127
+ const rows = ctx.configEditor.configuration()
128
+ const row = rows.find((item) => entryId(item?.entry) === 'agent-team')
129
+ if (row === undefined) {
130
+ await trace('attempt ' + attempt + ': agent-team row not found')
131
+ return true
132
+ }
133
+ if (alreadyApplied(row.override, desired)) {
134
+ await trace('attempt ' + attempt + ': already applied')
135
+ return false
136
+ }
137
+ await ctx.configEditor.edit(row.entry, () => ({ ...desired }))
138
+ await trace('attempt ' + attempt + ': applied ' + JSON.stringify(desired))
139
+ return false
140
+ } catch (error) {
141
+ const message = String((error && error.message) || error)
142
+ await trace('attempt ' + attempt + ': error=' + message)
143
+ return true
144
+ }
145
+ })
146
+ }
147
+
148
+ function schedule(ctx, desired, attempt) {
149
+ const delay = RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)]
150
+ setTimeout(() => {
151
+ void push(ctx, desired, attempt).then((retry) => {
152
+ if (retry && attempt + 1 < RETRY_DELAYS.length) schedule(ctx, desired, attempt + 1)
153
+ })
154
+ }, delay)
155
+ }
156
+
157
+ /** Volatile fields arrive as read-only references; ordinary ones as numbers. */
158
+ function readValue(value, fallback) {
159
+ if (value !== null && typeof value === 'object' && typeof value.get === 'function') {
160
+ const current = value.get()
161
+ return typeof current === 'number' ? current : fallback
162
+ }
163
+ return typeof value === 'number' ? value : fallback
164
+ }
165
+
166
+ /** The limits the page has saved, read from the live Config references. */
167
+ function currentValues(config) {
168
+ const raw = {}
169
+ for (const key of Object.keys(DEFAULTS)) raw[key] = readValue(config === null || config === undefined ? undefined : config[key], DEFAULTS[key])
170
+ return normalize(raw)
171
+ }
172
+
173
+ export function apply(ctx, config) {
174
+ /* This package ships its own page, so the harness must not auto-generate one. */
175
+ ctx.inject(['settings'], (child) => {
176
+ child.settings.configure({ auto: false }, ctx.fiber)
177
+ })
178
+ setTimeout(() => {
179
+ try {
180
+ const settings = ctx.get('settings')
181
+ const rows = settings === undefined ? [] : settings.describe()
182
+ void trace('namespaces=' + rows.map((row) => row.ns).join(','))
183
+ } catch (error) {
184
+ void trace('describe error=' + String((error && error.message) || error))
185
+ }
186
+ }, 3000)
187
+ schedule(ctx, currentValues(config), 0)
188
+ /*
189
+ * Volatile fields are edited in place without remounting this plugin, so the
190
+ * only way to learn about a saved form is to look at the references again.
191
+ * One timer, one comparison per tick, nothing while the values are unchanged.
192
+ */
193
+ ctx.effect(() => {
194
+ let last = JSON.stringify(currentValues(config))
195
+ const timer = setInterval(() => {
196
+ const next = currentValues(config)
197
+ const serialized = JSON.stringify(next)
198
+ if (serialized === last) return
199
+ last = serialized
200
+ void push(ctx, next, 1)
201
+ }, 1500)
202
+ return () => clearInterval(timer)
203
+ })
204
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Team Limits",
4
+ "description": "Adjust Agent Team's member count, task count, message size and teardown timeout."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "团队限额",
4
+ "description": "调整 Agent Team 的人数、任务数、单条消息大小与清理超时。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-team-limits",
3
- "version": "1.0.0",
4
- "description": "Optional companion bundle that raises the DeepSeek Harness Agent Team limits (members, tasks, message bytes). Installs only when you ask for it; it overrides the shipped agent-team row.",
3
+ "version": "1.3.4",
4
+ "description": "Optional companion bundle for the DeepSeek Harness Agent Team limits: members, tasks, message bytes and teardown timeout.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "SpaceRelay",
@@ -16,6 +16,26 @@
16
16
  "bugs": {
17
17
  "url": "https://github.com/SpaceRelay/dsh-subagent-control/issues"
18
18
  },
19
+ "type": "module",
20
+ "main": "./index.js",
21
+ "icon": "./icon.svg",
22
+ "exports": {
23
+ ".": "./index.js",
24
+ "./client": "./client.js",
25
+ "./package.json": "./package.json",
26
+ "./locale/*.json": "./locale/*.json",
27
+ "./icon": "./icon.svg"
28
+ },
29
+ "files": [
30
+ "index.js",
31
+ "client.js",
32
+ "cordis.patch.yml",
33
+ "locale/*.json",
34
+ "icon.svg",
35
+ "README.md",
36
+ "LICENSE",
37
+ "CHANGELOG.md"
38
+ ],
19
39
  "keywords": [
20
40
  "dsh",
21
41
  "dsh-plugin",
@@ -24,14 +44,18 @@
24
44
  "team",
25
45
  "limits"
26
46
  ],
27
- "files": [
28
- "cordis.patch.yml",
29
- "README.md",
30
- "LICENSE"
31
- ],
32
47
  "dsh": {
33
48
  "bundle": {
34
49
  "patch": "./cordis.patch.yml"
50
+ },
51
+ "client": {
52
+ "platform": "web",
53
+ "immediately": true,
54
+ "inject": [
55
+ "@deepseek-ai/dsh-api-remotes",
56
+ "@deepseek-ai/dsh-client-locale",
57
+ "@deepseek-ai/dsh-client-ui-settings"
58
+ ]
35
59
  }
36
60
  },
37
61
  "publishConfig": {