dsh-team-limits 1.0.1 → 1.3.5

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 CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.5
4
+
5
+ - Removed the leftover debugging path and the trace calls that appended to it on every load. The
6
+ package now carries no absolute paths and writes nothing outside the profile.
7
+
8
+ ## 1.3.4
9
+
10
+ - The panel description no longer explains where the values are stored. It names the four limits
11
+ and stops; the page itself shows how to edit them.
12
+
13
+ ## 1.3.3
14
+
15
+ - Dropped the "nothing else is touched" clause from the panel description in both languages. The
16
+ description now states what the page does and stops there.
17
+
18
+ ## 1.3.2
19
+
20
+ - The panel description now says what the bundle does (adjust the Agent Team limits from the plugin
21
+ page, written onto the agent-team row) instead of quoting one fixed set of numbers, which had
22
+ been stale since 1.2.0 turned the limits into editable fields.
23
+ - The README follows the same wording: it points at Settings → Plugins → Installed → Team limits
24
+ and no longer claims a Save button or a standalone Settings page.
25
+
26
+ ## 1.3.1
27
+
28
+ - The form now lives on the plugin's own page: opening "Team limits" in Plugins → Installed shows
29
+ the four fields right below the bundle description, through the native `plugins.bundle.config`
30
+ slot keyed by the package name. The extra tab in the Plugins section is gone.
31
+ - The compact `summary` view of the same slot renders one line (members · tasks · bytes · ms)
32
+ instead of the whole form.
33
+
34
+ ## 1.3.0
35
+
36
+ - The page now lives where the rest of a plugin's UI lives: **Settings → Plugins → Team limits**,
37
+ as a tab of the Plugins section, instead of a standalone row in the Settings sidebar.
38
+ - The page was rebuilt to match the shipped settings pages: a title and one-line introduction,
39
+ section headings, two columns, and per-field "Overridden" markers with a "Restore default"
40
+ action. Edits are applied as you type (debounced), so there is no Save button.
41
+ - The four numbers are grouped as "Runtime limits" (members, tasks) and "Messages and teardown"
42
+ (message bytes, disposal timeout).
43
+
44
+ ## 1.2.0
45
+
46
+ - New: a real settings page, **Settings → Team limits**, with four number fields, Save and
47
+ Restore defaults. Values are written onto the shipped `agent-team` row through the host's
48
+ config editor instead of being frozen in the bundle patch.
49
+ - The bundle patch no longer overrides `agent-team`; it only mounts the row, so a DSH upgrade
50
+ that changes the shipped defaults no longer fights with stale numbers in this file.
51
+ - Added the browser half (`client.js`, `dsh.client`) and a pinned `maxMembers: 32` starting
52
+ point; the page still lives in the harness Settings shell, so nothing renders in
53
+ terminal-only profiles.
54
+
3
55
  ## 1.0.1
4
56
 
5
57
  - Added a real entry artifact (`index.js`) and `main`/`exports`, fixing the plugin-manager
package/README.md CHANGED
@@ -2,36 +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) |
17
-
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.
18
+ | `disposalTimeoutMs` | 5000 | 5000 |
19
19
 
20
- The bundle also mounts one tiny component row (`Team Limits`) that registers nothing. It exists so the plugin panel can show the bundle and its localized description next to its switch, instead of listing it without a component.
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.
21
21
 
22
22
  ### Before you install
23
23
 
24
- - **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.
25
- - **Restart DSH after installing.** The row is read at startup.
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 actually need.
27
- - **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.
28
28
 
29
29
  ### Install
30
30
 
31
31
  Through the plugin panel / market, or by asking the agent:
32
32
 
33
33
  ```
34
- plugin_manager install_bundle target "dsh-team-limits@1.0.1"
34
+ plugin_manager install_bundle target "dsh-team-limits@1.3.2"
35
35
  ```
36
36
 
37
37
  ### License
@@ -42,34 +42,34 @@ MIT — see [LICENSE](./LICENSE). Issues: <https://github.com/SpaceRelay/dsh-sub
42
42
 
43
43
  ## 简体中文
44
44
 
45
- 这是 [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
46
 
47
- Agent Team 插件出厂限制如下:
47
+ ### 它做什么
48
48
 
49
- | 配置 | 出厂默认 | 本包 |
49
+ Agent Team 出厂限制如下:
50
+
51
+ | 配置 | 出厂默认 | 本页面初始值 |
50
52
  | --- | --- | --- |
51
53
  | `maxMembers` | 16 | **32** |
52
54
  | `maxTasks` | 256 | **512** |
53
55
  | `maxMessageBytes` | 65536 | **131072** |
54
- | `disposalTimeoutMs` | 5000 | 5000(不变) |
55
-
56
- 它不是一个带工具的插件,而是一个**只做覆盖的 bundle**:补丁指向现有的 `agent-team` 行,整体替换它的 `config`。不安装就完全不生效——把别人的上限改高应该是主动选择,不是默认行为。
56
+ | `disposalTimeoutMs` | 5000 | 5000 |
57
57
 
58
- 它另外会挂一个**只占位的小组件行**(团队限额),不注册任何东西;作用是让插件面板能把这一行、它的中文标题与说明和开关一起显示出来,而不是只显示一个没有组件的包。
58
+ 页面有四个数字输入框。改动边输边写(防抖,没有保存按钮),每个字段一旦偏离出厂值就会出现**恢复默认**。宿主侧的配置编辑器会把这些值写到自带的 `agent-team` 行上,持久化进 profile 的补丁层,并按正常加载流程生效。因此本包自己的补丁里不再存数值:DSH 升级改变了出厂默认,也不会悄悄把旧数字带回来。
59
59
 
60
60
  ### 安装前要知道
61
61
 
62
- - **补丁是整体替换 `config`,不是深合并。** 上面四个字段因此必须写全;如果宿主以后新增第五个字段,本包不会提供它,那个字段会回到默认值。
63
- - **安装后需要重启 DSH**,这一行在启动时读取。
64
- - **提高上限不是免费的。** 队友更多意味着更多并发的模型会话,每个都消耗 token 与一个会话目录;按实际需要调,不要一次拉满。
65
- - **卸载即恢复出厂默认**,不写任何其它位置。
62
+ - **安装后重启 DSH**,然后打开 设置 → 插件 → 已安装 → 团队限额;输入框就在插件描述下面。
63
+ - **数值存在 profile 的补丁层里。** 卸载本包不会把它们撤回;要恢复出厂值,请在页面上点「恢复默认」,或删除 profile 补丁里 `agent-team` 那条覆盖。
64
+ - **提高上限不是免费的。** 队友更多意味着更多并发的模型会话,每个都消耗 token 与一个会话目录;按实际需要调。
65
+ - 这个页面依赖 Harness 的浏览器端;纯终端 profile 不渲染页面,但限额照样生效。
66
66
 
67
67
  ### 安装
68
68
 
69
69
  在插件面板 / 市场中安装,或者让智能体执行:
70
70
 
71
71
  ```
72
- plugin_manager install_bundle target "dsh-team-limits@1.0.1"
72
+ plugin_manager install_bundle target "dsh-team-limits@1.3.2"
73
73
  ```
74
74
 
75
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,22 +1,11 @@
1
- # A configuration-only bundle: the real work is the agent-team override below.
2
- # The inserted row registers nothing; it exists so the bundle has a resolvable
3
- # entry artifact and one visible component in the plugin panel.
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
- # A patch that names an existing id replaces that row's whole `config`, so every
6
- # field is restated here; keep them in sync with the host's agent-team schema
7
- # when DSH changes.
8
- #
9
- # default -> this bundle
10
- # maxMembers 16 -> 32
11
- # maxTasks 256 -> 512
12
- # maxMessageBytes 65536 -> 131072
13
- # disposalTimeoutMs 5000 -> 5000 (unchanged)
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.
14
9
  - insert:
15
10
  - id: team-limits
16
11
  name: 'dsh-team-limits'
17
- - id: agent-team
18
- config:
19
- maxMembers: 32
20
- maxTasks: 512
21
- maxMessageBytes: 131072
22
- disposalTimeoutMs: 5000
package/index.js CHANGED
@@ -1,11 +1,179 @@
1
1
  /**
2
- * dsh-team-limits is a configuration-only bundle: the actual work happens in
3
- * cordis.patch.yml, which overrides the shipped `agent-team` row.
2
+ * dsh-team-limits — Host half.
4
3
  *
5
- * This module registers nothing. It exists so the bundle has a real entry
6
- * artifact and one visible component row in the plugin panel, which is how a
7
- * user can tell that the bundle is installed and enabled.
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.
8
16
  */
17
+
9
18
  export const name = 'dsh-team-limits'
10
19
 
11
- export function apply() {}
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
+ let Schema
32
+ try {
33
+ const loaded = await import('@deepseek-ai/schemastery')
34
+ Schema = loaded.default ?? loaded.Schema
35
+ } catch {
36
+ Schema = undefined
37
+ }
38
+
39
+ export const Config =
40
+ Schema === undefined
41
+ ? undefined
42
+ : Schema.object({
43
+ maxMembers: Schema.natural()
44
+ .min(1)
45
+ .volatile()
46
+ .default(DEFAULTS.maxMembers)
47
+ .description('Maximum number of teammates in one team.'),
48
+ maxTasks: Schema.natural()
49
+ .min(1)
50
+ .volatile()
51
+ .default(DEFAULTS.maxTasks)
52
+ .description('Maximum number of tasks the shared task board keeps.'),
53
+ maxMessageBytes: Schema.natural()
54
+ .min(1)
55
+ .volatile()
56
+ .default(DEFAULTS.maxMessageBytes)
57
+ .description('Maximum size of a single team message, in bytes (131072 = 128 KB).'),
58
+ disposalTimeoutMs: Schema.natural()
59
+ .min(1)
60
+ .volatile()
61
+ .default(DEFAULTS.disposalTimeoutMs)
62
+ .description('How long teardown waits for a member before giving up, in milliseconds.'),
63
+ }).description('Limits applied to the shipped Agent Team row.')
64
+
65
+ function normalize(config) {
66
+ const source = config ?? {}
67
+ const result = {}
68
+ for (const key of Object.keys(DEFAULTS)) {
69
+ const value = source[key]
70
+ result[key] =
71
+ typeof value === 'number' && Number.isFinite(value) && value >= 1
72
+ ? Math.floor(value)
73
+ : DEFAULTS[key]
74
+ }
75
+ return result
76
+ }
77
+
78
+ function entryId(entry) {
79
+ if (entry === null || entry === undefined) return ''
80
+ if (typeof entry === 'string') return entry
81
+ const candidates = [entry.options?.id, entry.id, entry.patch?.id, entry.entry?.id]
82
+ for (const candidate of candidates) {
83
+ if (typeof candidate === 'string' && candidate.length > 0) return candidate
84
+ }
85
+ return ''
86
+ }
87
+
88
+ function alreadyApplied(override, desired) {
89
+ if (override === null || override === undefined) return false
90
+ return Object.keys(desired).every((key) => override[key] === desired[key])
91
+ }
92
+
93
+ /**
94
+ * Run `work` outside any HMR transaction. The service keeps its flag in an
95
+ * AsyncLocalStorage; ours was inherited from the transaction that mounted this
96
+ * plugin, so only its own `exit` can clear it for a later asynchronous task.
97
+ */
98
+ function escapeTransaction(ctx, work) {
99
+ let executing
100
+ try {
101
+ const hmr = ctx.get('hmr')
102
+ executing = hmr === undefined ? undefined : hmr.executing
103
+ } catch {
104
+ executing = undefined
105
+ }
106
+ if (executing !== undefined && typeof executing.exit === 'function') {
107
+ return executing.exit(work)
108
+ }
109
+ return work()
110
+ }
111
+
112
+ /** @returns {Promise<boolean>} true when another attempt is worth making. */
113
+ async function push(ctx, desired) {
114
+ return escapeTransaction(ctx, async () => {
115
+ try {
116
+ const rows = ctx.configEditor.configuration()
117
+ const row = rows.find((item) => entryId(item?.entry) === 'agent-team')
118
+ if (row === undefined) {
119
+ return true
120
+ }
121
+ if (alreadyApplied(row.override, desired)) {
122
+ return false
123
+ }
124
+ await ctx.configEditor.edit(row.entry, () => ({ ...desired }))
125
+ return false
126
+ } catch {
127
+ return true
128
+ }
129
+ })
130
+ }
131
+
132
+ function schedule(ctx, desired, attempt) {
133
+ const delay = RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)]
134
+ setTimeout(() => {
135
+ void push(ctx, desired).then((retry) => {
136
+ if (retry && attempt + 1 < RETRY_DELAYS.length) schedule(ctx, desired, attempt + 1)
137
+ })
138
+ }, delay)
139
+ }
140
+
141
+ /** Volatile fields arrive as read-only references; ordinary ones as numbers. */
142
+ function readValue(value, fallback) {
143
+ if (value !== null && typeof value === 'object' && typeof value.get === 'function') {
144
+ const current = value.get()
145
+ return typeof current === 'number' ? current : fallback
146
+ }
147
+ return typeof value === 'number' ? value : fallback
148
+ }
149
+
150
+ /** The limits the page has saved, read from the live Config references. */
151
+ function currentValues(config) {
152
+ const raw = {}
153
+ for (const key of Object.keys(DEFAULTS)) raw[key] = readValue(config === null || config === undefined ? undefined : config[key], DEFAULTS[key])
154
+ return normalize(raw)
155
+ }
156
+
157
+ export function apply(ctx, config) {
158
+ /* This package ships its own page, so the harness must not auto-generate one. */
159
+ ctx.inject(['settings'], (child) => {
160
+ child.settings.configure({ auto: false }, ctx.fiber)
161
+ })
162
+ schedule(ctx, currentValues(config), 0)
163
+ /*
164
+ * Volatile fields are edited in place without remounting this plugin, so the
165
+ * only way to learn about a saved form is to look at the references again.
166
+ * One timer, one comparison per tick, nothing while the values are unchanged.
167
+ */
168
+ ctx.effect(() => {
169
+ let last = JSON.stringify(currentValues(config))
170
+ const timer = setInterval(() => {
171
+ const next = currentValues(config)
172
+ const serialized = JSON.stringify(next)
173
+ if (serialized === last) return
174
+ last = serialized
175
+ void push(ctx, next, 1)
176
+ }, 1500)
177
+ return () => clearInterval(timer)
178
+ })
179
+ }
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Team Limits",
4
- "description": "Raises the Agent Team limits: 32 members, 512 tasks, 128 KB messages. Overrides only the agent-team row; nothing else changes."
4
+ "description": "Adjust Agent Team's member count, task count, message size and teardown timeout."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "团队限额",
4
- "description": "把 Agent Team 的上限调高:成员 32、任务 512、单条消息 128 KB。只覆盖 agent-team 这一行,其它都不改。"
4
+ "description": "调整 Agent Team 的人数、任务数、单条消息大小与清理超时。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-team-limits",
3
- "version": "1.0.1",
4
- "description": "Optional companion bundle that raises the DeepSeek Harness Agent Team limits (members, tasks, message bytes). It overrides the shipped agent-team row and shows one component row in the plugin panel.",
3
+ "version": "1.3.5",
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",
@@ -21,12 +21,14 @@
21
21
  "icon": "./icon.svg",
22
22
  "exports": {
23
23
  ".": "./index.js",
24
+ "./client": "./client.js",
24
25
  "./package.json": "./package.json",
25
26
  "./locale/*.json": "./locale/*.json",
26
27
  "./icon": "./icon.svg"
27
28
  },
28
29
  "files": [
29
30
  "index.js",
31
+ "client.js",
30
32
  "cordis.patch.yml",
31
33
  "locale/*.json",
32
34
  "icon.svg",
@@ -45,6 +47,15 @@
45
47
  "dsh": {
46
48
  "bundle": {
47
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
+ ]
48
59
  }
49
60
  },
50
61
  "publishConfig": {