dsh-auto-compaction 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +479 -0
- package/lib/client.js +70 -42
- package/lib/index.js +68 -29
- package/lib/takeover-mount.js +306 -0
- package/package.json +2 -2
package/cordis.patch.yml
CHANGED
|
@@ -8,3 +8,482 @@
|
|
|
8
8
|
- insert:
|
|
9
9
|
- id: dsh-auto-compaction
|
|
10
10
|
name: dsh-auto-compaction
|
|
11
|
+
# >>> handoff:takeover:begin (dsh-auto-compaction 按当前预设组合生成,勿手改本块)
|
|
12
|
+
- id: preset-cordis
|
|
13
|
+
config:
|
|
14
|
+
id: cordis
|
|
15
|
+
order: 4
|
|
16
|
+
plugins:
|
|
17
|
+
- id: persona
|
|
18
|
+
name: '@deepseek-ai/dsh-persona'
|
|
19
|
+
config:
|
|
20
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
21
|
+
- id: agent-instructions
|
|
22
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
23
|
+
config:
|
|
24
|
+
maxBytes: 65536
|
|
25
|
+
- id: time-context
|
|
26
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
27
|
+
- id: tool-bash
|
|
28
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
29
|
+
disabled: !!js process.platform === 'win32'
|
|
30
|
+
- id: tool-pwsh
|
|
31
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
32
|
+
disabled: !!js process.platform !== 'win32'
|
|
33
|
+
- id: tool-fs
|
|
34
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
35
|
+
- id: tool-fs-search
|
|
36
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
37
|
+
config:
|
|
38
|
+
sampleOverCapGlobResults: false
|
|
39
|
+
- id: tool-jobs
|
|
40
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
41
|
+
- id: tool-schedule
|
|
42
|
+
name: '@deepseek-ai/dsh-tool-schedule'
|
|
43
|
+
- id: command-goal
|
|
44
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
45
|
+
- id: tool-goal
|
|
46
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
47
|
+
- id: planning
|
|
48
|
+
name: cordis:group
|
|
49
|
+
group: true
|
|
50
|
+
isolate:
|
|
51
|
+
planMode: true
|
|
52
|
+
config:
|
|
53
|
+
- id: plan-mode
|
|
54
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
55
|
+
config:
|
|
56
|
+
section: |
|
|
57
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
58
|
+
|
|
59
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
60
|
+
|
|
61
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
62
|
+
|
|
63
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
64
|
+
|
|
65
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
66
|
+
|
|
67
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
68
|
+
- id: compaction
|
|
69
|
+
name: cordis:group
|
|
70
|
+
group: true
|
|
71
|
+
isolate:
|
|
72
|
+
compaction: true
|
|
73
|
+
toolResultPruner: true
|
|
74
|
+
config:
|
|
75
|
+
- id: compaction-basic
|
|
76
|
+
name: dsh-auto-compaction-engine
|
|
77
|
+
config:
|
|
78
|
+
thresholdRatio: 0.9
|
|
79
|
+
headroomTokens: 10000
|
|
80
|
+
maxTokens: 12288
|
|
81
|
+
idleDelayMs: 1500
|
|
82
|
+
presetId: cordis
|
|
83
|
+
- id: command-compact
|
|
84
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
85
|
+
- id: tool-result-pruner
|
|
86
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
87
|
+
config:
|
|
88
|
+
thresholdChars: 8192
|
|
89
|
+
headChars: 4096
|
|
90
|
+
tailChars: 1024
|
|
91
|
+
- id: delegation
|
|
92
|
+
name: cordis:group
|
|
93
|
+
group: true
|
|
94
|
+
isolate:
|
|
95
|
+
workflowEngine: true
|
|
96
|
+
config:
|
|
97
|
+
- id: tool-subagent-control
|
|
98
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
99
|
+
- id: tool-subagent-list-agents
|
|
100
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
101
|
+
- id: tool-subagent
|
|
102
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
103
|
+
config:
|
|
104
|
+
provider: spawn
|
|
105
|
+
toolName: subagent
|
|
106
|
+
modelSelectionSettings: true
|
|
107
|
+
- id: tool-subagent-fork
|
|
108
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
109
|
+
config:
|
|
110
|
+
provider: fork
|
|
111
|
+
toolName: subagent_fork
|
|
112
|
+
- id: workflow-ptc
|
|
113
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
114
|
+
config:
|
|
115
|
+
provider: spawn
|
|
116
|
+
- id: tool-workflow
|
|
117
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
118
|
+
- id: tool-ask-user
|
|
119
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
120
|
+
- id: tool-todo
|
|
121
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
122
|
+
config:
|
|
123
|
+
allowParallelInProgress: true
|
|
124
|
+
- id: tool-web
|
|
125
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
126
|
+
config:
|
|
127
|
+
fetch: true
|
|
128
|
+
searchTimeoutMs: 60000
|
|
129
|
+
- id: tool-cordis
|
|
130
|
+
name: '@deepseek-ai/dsh-tool-cordis'
|
|
131
|
+
- id: skill-filesystem
|
|
132
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
133
|
+
config:
|
|
134
|
+
customSkillDirs:
|
|
135
|
+
- !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@deepseek-ai/dsh-agent-preset/package.json')), 'skills')
|
|
136
|
+
- id: tool-skill
|
|
137
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
138
|
+
- id: present
|
|
139
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
140
|
+
- id: tool-plugin-manager
|
|
141
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
142
|
+
disabled: !!js '!ctx.get(''profileContext'')'
|
|
143
|
+
- id: tool-plugin-market
|
|
144
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
145
|
+
disabled: !!js '!ctx.get(''profileContext'')'
|
|
146
|
+
- id: preset-minimal
|
|
147
|
+
config:
|
|
148
|
+
id: minimal
|
|
149
|
+
order: 3
|
|
150
|
+
plugins:
|
|
151
|
+
- id: persona
|
|
152
|
+
name: '@deepseek-ai/dsh-persona'
|
|
153
|
+
config:
|
|
154
|
+
prefix: You are a helpful software engineer assistant.
|
|
155
|
+
complete: true
|
|
156
|
+
includeRuntimeContext: false
|
|
157
|
+
- id: tool-fs
|
|
158
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
159
|
+
config:
|
|
160
|
+
read: false
|
|
161
|
+
readImage: true
|
|
162
|
+
write: false
|
|
163
|
+
edit: false
|
|
164
|
+
- id: compaction
|
|
165
|
+
name: cordis:group
|
|
166
|
+
group: true
|
|
167
|
+
isolate:
|
|
168
|
+
compaction: true
|
|
169
|
+
config:
|
|
170
|
+
- id: compaction-basic
|
|
171
|
+
name: dsh-auto-compaction-engine
|
|
172
|
+
config:
|
|
173
|
+
thresholdRatio: 0.9
|
|
174
|
+
headroomTokens: 10000
|
|
175
|
+
maxTokens: 12288
|
|
176
|
+
idleDelayMs: 1500
|
|
177
|
+
presetId: minimal
|
|
178
|
+
- id: command-compact
|
|
179
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
180
|
+
- id: persistent-shell
|
|
181
|
+
name: cordis:group
|
|
182
|
+
group: true
|
|
183
|
+
isolate:
|
|
184
|
+
terminals: true
|
|
185
|
+
config:
|
|
186
|
+
- id: pty
|
|
187
|
+
name: '@deepseek-ai/dsh-terminal'
|
|
188
|
+
- id: terminal-bash
|
|
189
|
+
name: '@deepseek-ai/dsh-terminal-bash'
|
|
190
|
+
disabled: !!js process.platform === 'win32'
|
|
191
|
+
config:
|
|
192
|
+
timeoutMs: 300000
|
|
193
|
+
- id: persistent-bash
|
|
194
|
+
name: '@deepseek-ai/dsh-tool-bash-persistent'
|
|
195
|
+
disabled: !!js process.platform === 'win32'
|
|
196
|
+
config:
|
|
197
|
+
timeoutMs: 300000
|
|
198
|
+
description: |-
|
|
199
|
+
Run commands in a bash shell
|
|
200
|
+
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
|
201
|
+
* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.
|
|
202
|
+
* State is persistent across command calls and discussions with the user.
|
|
203
|
+
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
|
|
204
|
+
* Please avoid commands that may produce a very large amount of output.
|
|
205
|
+
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
|
|
206
|
+
- id: terminal-pwsh
|
|
207
|
+
name: '@deepseek-ai/dsh-terminal-bash'
|
|
208
|
+
disabled: !!js process.platform !== 'win32'
|
|
209
|
+
config:
|
|
210
|
+
shellDialect: pwsh
|
|
211
|
+
timeoutMs: 300000
|
|
212
|
+
- id: persistent-pwsh
|
|
213
|
+
name: '@deepseek-ai/dsh-tool-pwsh-persistent'
|
|
214
|
+
disabled: !!js process.platform !== 'win32'
|
|
215
|
+
config:
|
|
216
|
+
timeoutMs: 300000
|
|
217
|
+
description: |-
|
|
218
|
+
Run commands in a PowerShell shell
|
|
219
|
+
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
|
220
|
+
* You don't have access to the internet via this tool.
|
|
221
|
+
* State is persistent across command calls and discussions with the user.
|
|
222
|
+
* Use native Windows paths (C:\...) and $env:NAME variables; this is PowerShell, not bash.
|
|
223
|
+
* Please avoid commands that may produce a very large amount of output.
|
|
224
|
+
* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.
|
|
225
|
+
- id: preset-ptc
|
|
226
|
+
config:
|
|
227
|
+
id: ptc
|
|
228
|
+
order: 2
|
|
229
|
+
plugins:
|
|
230
|
+
- id: persona
|
|
231
|
+
name: '@deepseek-ai/dsh-persona'
|
|
232
|
+
config:
|
|
233
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
234
|
+
- id: agent-instructions
|
|
235
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
236
|
+
config:
|
|
237
|
+
maxBytes: 65536
|
|
238
|
+
- id: time-context
|
|
239
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
240
|
+
- id: tool-bash
|
|
241
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
242
|
+
disabled: !!js process.platform === 'win32'
|
|
243
|
+
- id: tool-pwsh
|
|
244
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
245
|
+
disabled: !!js process.platform !== 'win32'
|
|
246
|
+
- id: tool-fs
|
|
247
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
248
|
+
- id: tool-fs-search
|
|
249
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
250
|
+
config:
|
|
251
|
+
sampleOverCapGlobResults: false
|
|
252
|
+
- id: tool-jobs
|
|
253
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
254
|
+
- id: tool-schedule
|
|
255
|
+
name: '@deepseek-ai/dsh-tool-schedule'
|
|
256
|
+
- id: skill-filesystem
|
|
257
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
258
|
+
- id: tool-skill
|
|
259
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
260
|
+
- id: command-goal
|
|
261
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
262
|
+
- id: tool-goal
|
|
263
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
264
|
+
- id: planning
|
|
265
|
+
name: cordis:group
|
|
266
|
+
group: true
|
|
267
|
+
isolate:
|
|
268
|
+
planMode: true
|
|
269
|
+
config:
|
|
270
|
+
- id: plan-mode
|
|
271
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
272
|
+
config:
|
|
273
|
+
section: |
|
|
274
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
275
|
+
|
|
276
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
277
|
+
|
|
278
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
279
|
+
|
|
280
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
281
|
+
|
|
282
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
283
|
+
|
|
284
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
285
|
+
- id: compaction
|
|
286
|
+
name: cordis:group
|
|
287
|
+
group: true
|
|
288
|
+
isolate:
|
|
289
|
+
compaction: true
|
|
290
|
+
toolResultPruner: true
|
|
291
|
+
config:
|
|
292
|
+
- id: compaction-basic
|
|
293
|
+
name: dsh-auto-compaction-engine
|
|
294
|
+
config:
|
|
295
|
+
thresholdRatio: 0.9
|
|
296
|
+
headroomTokens: 10000
|
|
297
|
+
maxTokens: 12288
|
|
298
|
+
idleDelayMs: 1500
|
|
299
|
+
presetId: ptc
|
|
300
|
+
- id: command-compact
|
|
301
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
302
|
+
- id: tool-result-pruner
|
|
303
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
304
|
+
config:
|
|
305
|
+
thresholdChars: 8192
|
|
306
|
+
headChars: 4096
|
|
307
|
+
tailChars: 1024
|
|
308
|
+
- id: delegation
|
|
309
|
+
name: cordis:group
|
|
310
|
+
group: true
|
|
311
|
+
isolate:
|
|
312
|
+
workflowEngine: true
|
|
313
|
+
config:
|
|
314
|
+
- id: tool-subagent-control
|
|
315
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
316
|
+
- id: tool-subagent-list-agents
|
|
317
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
318
|
+
- id: tool-subagent
|
|
319
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
320
|
+
config:
|
|
321
|
+
provider: spawn
|
|
322
|
+
toolName: subagent
|
|
323
|
+
modelSelectionSettings: true
|
|
324
|
+
- id: tool-subagent-fork
|
|
325
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
326
|
+
config:
|
|
327
|
+
provider: fork
|
|
328
|
+
toolName: subagent_fork
|
|
329
|
+
- id: workflow-ptc
|
|
330
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
331
|
+
disabled: true
|
|
332
|
+
config:
|
|
333
|
+
provider: spawn
|
|
334
|
+
- id: tool-workflow
|
|
335
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
336
|
+
disabled: true
|
|
337
|
+
- id: tool-ask-user
|
|
338
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
339
|
+
- id: tool-todo
|
|
340
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
341
|
+
config:
|
|
342
|
+
allowParallelInProgress: true
|
|
343
|
+
- id: tool-web
|
|
344
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
345
|
+
config:
|
|
346
|
+
fetch: true
|
|
347
|
+
searchTimeoutMs: 60000
|
|
348
|
+
- id: tool-presentation
|
|
349
|
+
name: '@deepseek-ai/dsh-agent-tool-presentation'
|
|
350
|
+
config:
|
|
351
|
+
mode: ptc
|
|
352
|
+
- id: present
|
|
353
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
354
|
+
- id: tool-plugin-manager
|
|
355
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
356
|
+
disabled: true
|
|
357
|
+
- id: tool-plugin-market
|
|
358
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
359
|
+
disabled: true
|
|
360
|
+
- id: preset-standard
|
|
361
|
+
config:
|
|
362
|
+
id: standard
|
|
363
|
+
order: 1
|
|
364
|
+
plugins:
|
|
365
|
+
- id: persona
|
|
366
|
+
name: '@deepseek-ai/dsh-persona'
|
|
367
|
+
config:
|
|
368
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
369
|
+
- id: agent-instructions
|
|
370
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
371
|
+
config:
|
|
372
|
+
maxBytes: 65536
|
|
373
|
+
- id: time-context
|
|
374
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
375
|
+
- id: tool-bash
|
|
376
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
377
|
+
disabled: !!js process.platform === 'win32'
|
|
378
|
+
- id: tool-pwsh
|
|
379
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
380
|
+
disabled: !!js process.platform !== 'win32'
|
|
381
|
+
- id: tool-fs
|
|
382
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
383
|
+
- id: tool-fs-search
|
|
384
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
385
|
+
config:
|
|
386
|
+
sampleOverCapGlobResults: false
|
|
387
|
+
- id: tool-jobs
|
|
388
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
389
|
+
- id: tool-schedule
|
|
390
|
+
name: '@deepseek-ai/dsh-tool-schedule'
|
|
391
|
+
- id: skill-filesystem
|
|
392
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
393
|
+
- id: tool-skill
|
|
394
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
395
|
+
- id: command-goal
|
|
396
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
397
|
+
- id: tool-goal
|
|
398
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
399
|
+
- id: planning
|
|
400
|
+
name: cordis:group
|
|
401
|
+
group: true
|
|
402
|
+
isolate:
|
|
403
|
+
planMode: true
|
|
404
|
+
config:
|
|
405
|
+
- id: plan-mode
|
|
406
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
407
|
+
config:
|
|
408
|
+
section: |
|
|
409
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
410
|
+
|
|
411
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
412
|
+
|
|
413
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
414
|
+
|
|
415
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
416
|
+
|
|
417
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
418
|
+
|
|
419
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
420
|
+
- id: compaction
|
|
421
|
+
name: cordis:group
|
|
422
|
+
group: true
|
|
423
|
+
isolate:
|
|
424
|
+
compaction: true
|
|
425
|
+
toolResultPruner: true
|
|
426
|
+
config:
|
|
427
|
+
- id: compaction-basic
|
|
428
|
+
name: dsh-auto-compaction-engine
|
|
429
|
+
config:
|
|
430
|
+
thresholdRatio: 0.9
|
|
431
|
+
headroomTokens: 10000
|
|
432
|
+
maxTokens: 12288
|
|
433
|
+
idleDelayMs: 1500
|
|
434
|
+
presetId: standard
|
|
435
|
+
- id: command-compact
|
|
436
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
437
|
+
- id: tool-result-pruner
|
|
438
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
439
|
+
config:
|
|
440
|
+
thresholdChars: 8192
|
|
441
|
+
headChars: 4096
|
|
442
|
+
tailChars: 1024
|
|
443
|
+
- id: delegation
|
|
444
|
+
name: cordis:group
|
|
445
|
+
group: true
|
|
446
|
+
isolate:
|
|
447
|
+
workflowEngine: true
|
|
448
|
+
config:
|
|
449
|
+
- id: tool-subagent-control
|
|
450
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
451
|
+
- id: tool-subagent-list-agents
|
|
452
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
453
|
+
- id: tool-subagent
|
|
454
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
455
|
+
config:
|
|
456
|
+
provider: spawn
|
|
457
|
+
toolName: subagent
|
|
458
|
+
modelSelectionSettings: true
|
|
459
|
+
- id: tool-subagent-fork
|
|
460
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
461
|
+
config:
|
|
462
|
+
provider: fork
|
|
463
|
+
toolName: subagent_fork
|
|
464
|
+
- id: workflow-ptc
|
|
465
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
466
|
+
config:
|
|
467
|
+
provider: spawn
|
|
468
|
+
- id: tool-workflow
|
|
469
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
470
|
+
- id: tool-ask-user
|
|
471
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
472
|
+
- id: tool-todo
|
|
473
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
474
|
+
config:
|
|
475
|
+
allowParallelInProgress: true
|
|
476
|
+
- id: tool-web
|
|
477
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
478
|
+
config:
|
|
479
|
+
fetch: true
|
|
480
|
+
searchTimeoutMs: 60000
|
|
481
|
+
- id: present
|
|
482
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
483
|
+
- id: tool-plugin-manager
|
|
484
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
485
|
+
disabled: true
|
|
486
|
+
- id: tool-plugin-market
|
|
487
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
488
|
+
disabled: true
|
|
489
|
+
# <<< handoff:takeover:end
|
package/lib/client.js
CHANGED
|
@@ -32,10 +32,21 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
var DEFAULT_TEMPLATE = "~/.dsh-handoff-compaction/template.md";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/* Preset names come from DSH's own registry through the `scope` RPC, never
|
|
36
|
+
* from a list baked in here: a release that renames or adds a preset must
|
|
37
|
+
* not leave this page showing names that no longer exist. */
|
|
38
|
+
var presetLabels = {};
|
|
39
|
+
function rememberPresetLabels(list) {
|
|
40
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
41
|
+
var entry = list[i];
|
|
42
|
+
if (entry && typeof entry.id === "string") {
|
|
43
|
+
presetLabels[entry.id] = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : entry.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function presetLabel(id) {
|
|
48
|
+
return presetLabels[String(id)] || String(id || "未命名预设");
|
|
49
|
+
}
|
|
39
50
|
var DISPLAY_OPTIONS = [
|
|
40
51
|
{ id: "orbit", label: "轨道环" },
|
|
41
52
|
{ id: "strip", label: "状态条" },
|
|
@@ -43,10 +54,6 @@ window.__ModuleLoader__.load({
|
|
|
43
54
|
{ id: "topbar", label: "顶部条" },
|
|
44
55
|
{ id: "corner", label: "右上角" },
|
|
45
56
|
];
|
|
46
|
-
function presetLabel(id) {
|
|
47
|
-
for (var i = 0; i < PRESETS.length; i += 1) if (PRESETS[i].id === id) return PRESETS[i].label;
|
|
48
|
-
return String(id || "未命名预设");
|
|
49
|
-
}
|
|
50
57
|
var EXAMPLE_WINDOW = 250000, EXAMPLE_RESERVED = 20000, EXAMPLE_HEADROOM = 10000;
|
|
51
58
|
|
|
52
59
|
var DISPLAY_STYLES = ["orbit", "strip", "ring", "topbar", "corner"];
|
|
@@ -697,8 +704,8 @@ window.__ModuleLoader__.load({
|
|
|
697
704
|
var token = tokenState[0], setToken = tokenState[1];
|
|
698
705
|
ensureStatusStyles();
|
|
699
706
|
|
|
700
|
-
//
|
|
701
|
-
// so
|
|
707
|
+
// Which presets exist and which of them this plugin's patch reaches is
|
|
708
|
+
// DSH's answer, not ours, so it is read fresh each time the section opens.
|
|
702
709
|
React.useEffect(function () {
|
|
703
710
|
if (!connection) {
|
|
704
711
|
setScope({ loading: false, error: "连接服务不可用", value: null });
|
|
@@ -707,7 +714,9 @@ window.__ModuleLoader__.load({
|
|
|
707
714
|
var alive = true;
|
|
708
715
|
setScope(function (previous) { return { loading: true, error: "", value: previous.value }; });
|
|
709
716
|
call(connection, "scope").then(function (value) {
|
|
710
|
-
if (alive)
|
|
717
|
+
if (!alive) return;
|
|
718
|
+
rememberPresetLabels(value && Array.isArray(value.presets) ? value.presets : []);
|
|
719
|
+
setScope({ loading: false, error: "", value: value });
|
|
711
720
|
}).catch(function (error) {
|
|
712
721
|
if (alive) setScope({ loading: false, error: String(error && error.message || error), value: null });
|
|
713
722
|
});
|
|
@@ -755,51 +764,61 @@ window.__ModuleLoader__.load({
|
|
|
755
764
|
tone: "err",
|
|
756
765
|
text: scope.loading ? "读取中…" : "读不到生效情况",
|
|
757
766
|
sub: scope.loading
|
|
758
|
-
? "
|
|
759
|
-
: "
|
|
767
|
+
? "正在向 DSH 读取预设名单与本插件的接管行。"
|
|
768
|
+
: "读不到预设名单:" + (scope.error || "未知原因") + "。",
|
|
760
769
|
rows: [], retry: true,
|
|
761
770
|
};
|
|
762
771
|
}
|
|
763
772
|
var list = Array.isArray(st.presets) ? st.presets : [];
|
|
773
|
+
rememberPresetLabels(list);
|
|
764
774
|
var rows = list.map(function (p) {
|
|
765
775
|
return {
|
|
766
|
-
key: String(p.id),
|
|
767
|
-
|
|
776
|
+
key: String(p.id),
|
|
777
|
+
label: presetLabel(p.id),
|
|
778
|
+
taken: p.effective === true,
|
|
779
|
+
owner: p.effective === true ? "插件" : p.covered === true ? "原生(未勾选)" : "原生(未覆盖)",
|
|
768
780
|
};
|
|
769
781
|
});
|
|
770
782
|
var names = rows.map(function (r) { return r.label; });
|
|
771
783
|
var takenNames = rows.filter(function (r) { return r.taken; }).map(function (r) { return r.label; });
|
|
772
784
|
var restNames = rows.filter(function (r) { return !r.taken; }).map(function (r) { return r.label; });
|
|
773
|
-
var taken = Number(st.taken) || 0;
|
|
774
785
|
var total = Number(st.total) || list.length;
|
|
786
|
+
var covered = Number(st.covered) || 0;
|
|
787
|
+
var effective = Number(st.effective) || 0;
|
|
775
788
|
var globalSaved = st.scope !== "preset";
|
|
776
|
-
var scopeLead = globalSaved
|
|
777
|
-
|
|
778
|
-
|
|
789
|
+
var scopeLead = globalSaved ? "全局生效:" : "按预设生效:";
|
|
790
|
+
var mechanism = "接管行写在本插件包内的补丁里,卸载插件时随包一起消失,你的预设文件不会被改动。";
|
|
791
|
+
|
|
779
792
|
if (total === 0) {
|
|
780
793
|
return {
|
|
781
794
|
tone: "err", text: "读不到预设", rows: [], retry: true,
|
|
782
|
-
sub: "
|
|
795
|
+
sub: "DSH 没有返回任何 Agent 预设" + (st.reason ? "(" + st.reason + ")" : "") + ",所以无法判断压缩由谁负责。重新读取试试。",
|
|
783
796
|
};
|
|
784
797
|
}
|
|
785
|
-
if (
|
|
798
|
+
if (covered === 0) {
|
|
786
799
|
return {
|
|
787
|
-
tone: "native", text: "DSH 原生压缩", rows: rows, retry:
|
|
788
|
-
sub: "
|
|
800
|
+
tone: "native", text: "DSH 原生压缩", rows: rows, retry: true,
|
|
801
|
+
sub: "本插件的接管行还没覆盖到任何预设" + (st.reason ? "(" + st.reason + ")" : "")
|
|
802
|
+
+ ",压缩目前由 DSH 自己完成。" + mechanism + " 新生成或新装的预设需要重启 DSH 重新生成接管行。",
|
|
789
803
|
};
|
|
790
804
|
}
|
|
791
|
-
if (!enabled) {
|
|
805
|
+
if (effective === 0 && !enabled) {
|
|
792
806
|
return {
|
|
793
807
|
tone: "native", text: "DSH 原生压缩(插件已停用)", rows: rows, retry: false,
|
|
794
|
-
sub:
|
|
795
|
-
|
|
796
|
-
|
|
808
|
+
sub: "「启用插件」已关闭:" + takenNames.length + "/" + total + " 个预设虽然被接管行覆盖,但插件把压缩交回 DSH 原生实现。打开开关即恢复,不需要重启。",
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (effective === 0) {
|
|
812
|
+
return {
|
|
813
|
+
tone: "native", text: "DSH 原生压缩", rows: rows, retry: false,
|
|
814
|
+
sub: scopeLead + "当前没有任何预设处于接管状态(" + names.join("、") + " 都走原生)。"
|
|
815
|
+
+ "勾选预设或切到「全局生效」并保存即可,下一次判定就生效,不需要重启。",
|
|
797
816
|
};
|
|
798
817
|
}
|
|
799
|
-
if (
|
|
818
|
+
if (effective < total) {
|
|
800
819
|
return {
|
|
801
|
-
tone: "part", text:
|
|
802
|
-
sub: scopeLead + "
|
|
820
|
+
tone: "part", text: effective + "/" + total + " 预设已由插件压缩", rows: rows, retry: false,
|
|
821
|
+
sub: scopeLead + "由插件负责:" + takenNames.join("、") + ";" + restNames.join("、") + " 仍走各自的原生压缩。" + mechanism,
|
|
803
822
|
};
|
|
804
823
|
}
|
|
805
824
|
return {
|
|
@@ -807,12 +826,17 @@ window.__ModuleLoader__.load({
|
|
|
807
826
|
text: globalSaved ? "已由插件接管(全局)" : "已由插件接管",
|
|
808
827
|
rows: rows, retry: false,
|
|
809
828
|
sub: scopeLead + (globalSaved
|
|
810
|
-
? "这 " + total + "
|
|
811
|
-
: "勾选的 " +
|
|
812
|
-
+ "
|
|
829
|
+
? "这 " + total + " 个预设(" + names.join("、") + ")"
|
|
830
|
+
: "勾选的 " + effective + " 个预设(" + takenNames.join("、") + ")")
|
|
831
|
+
+ " 的压缩由本插件的交接文档负责,且「启用插件」是开的。" + mechanism,
|
|
813
832
|
};
|
|
814
833
|
})();
|
|
815
834
|
var scopeTone = SCOPE_TONE[scopeView.tone] || SCOPE_TONE.native;
|
|
835
|
+
/* Only presets the takeover rows reach can be selected; the rest stay
|
|
836
|
+
* native and would be misleading to offer. */
|
|
837
|
+
var takeable = scope.value && Array.isArray(scope.value.presets)
|
|
838
|
+
? scope.value.presets.filter(function (p) { return p && p.covered === true; })
|
|
839
|
+
: [];
|
|
816
840
|
|
|
817
841
|
var budget = EXAMPLE_WINDOW - EXAMPLE_RESERVED;
|
|
818
842
|
var ceiling = budget - EXAMPLE_HEADROOM;
|
|
@@ -836,7 +860,7 @@ window.__ModuleLoader__.load({
|
|
|
836
860
|
if (scopeIsPending) pendingParts.push("生效范围改成「" + (scopePreset ? "按预设生效" : "全局生效") + "」");
|
|
837
861
|
if (presetsIsPending) pendingParts.push("勾选的预设改成" + (presets.length ? presets.map(presetLabel).join("、") : "(都不勾)"));
|
|
838
862
|
var pendingNote = pendingParts.length
|
|
839
|
-
? "还没保存:" + pendingParts.join(";") + "
|
|
863
|
+
? "还没保存:" + pendingParts.join(";") + "。保存后开关与勾选在下一次判定就生效,不需要重启;只有接管行本身要覆盖新的预设时才需要重启 DSH 重新生成。"
|
|
840
864
|
: "";
|
|
841
865
|
|
|
842
866
|
function patch(next) { setDraft(Object.assign({}, base, next)); setStatus({ loading: false, error: "", saving: false, saved: false }); }
|
|
@@ -906,7 +930,7 @@ window.__ModuleLoader__.load({
|
|
|
906
930
|
h("div", { style: rowStyle },
|
|
907
931
|
h("div", null,
|
|
908
932
|
h("div", { style: labelStyle }, "生效范围"),
|
|
909
|
-
h("div", { style: subStyle }, scopePreset ? "
|
|
933
|
+
h("div", { style: subStyle }, scopePreset ? "只有勾选的预设由插件压缩,其余预设保持 DSH 原生行为;名单来自 DSH 的预设注册表" : "所有能被本插件接管的预设都由插件压缩,共用这份设置")
|
|
910
934
|
),
|
|
911
935
|
h("div", { style: { display: "inline-flex", border: "1px solid " + C.line, borderRadius: 8, overflow: "hidden" } },
|
|
912
936
|
seg(!scopePreset, "全局生效", function () { patch({ scope: "global" }); }),
|
|
@@ -914,13 +938,17 @@ window.__ModuleLoader__.load({
|
|
|
914
938
|
)
|
|
915
939
|
),
|
|
916
940
|
scopePreset ? h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", padding: "10px 0", borderBottom: "1px solid " + C.line } },
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
941
|
+
takeable.length === 0
|
|
942
|
+
? h("span", { style: subStyle }, scope.loading
|
|
943
|
+
? "正在向 DSH 读取预设名单…"
|
|
944
|
+
: "本插件的接管行还没覆盖到任何预设。重启 DSH 会重新生成接管行;仍未出现预设名单说明 DSH 没有返回预设。")
|
|
945
|
+
: takeable.map(function (p) {
|
|
946
|
+
var on = presets.indexOf(p.id) >= 0;
|
|
947
|
+
return h("button", {
|
|
948
|
+
key: p.id, type: "button", onClick: function () { setPreset(p.id); },
|
|
949
|
+
style: { fontSize: 12, padding: "5px 11px", borderRadius: 999, cursor: "pointer", font: "inherit", border: "1px solid " + (on ? C.accent : C.line), background: on ? "rgba(76,141,255,.12)" : "#191a1d", color: on ? C.text : C.muted },
|
|
950
|
+
}, presetLabel(p.id));
|
|
951
|
+
})
|
|
924
952
|
) : null,
|
|
925
953
|
|
|
926
954
|
h("div", { style: rowStyle },
|
package/lib/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createRequire } from "node:module";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
19
|
import { execFile, spawn } from "node:child_process";
|
|
20
|
+
import { mountTakeover, presetInventory } from "./takeover-mount.js";
|
|
20
21
|
|
|
21
22
|
export const name = "dsh-auto-compaction";
|
|
22
23
|
export const inject = [];
|
|
@@ -25,7 +26,7 @@ export const Config = z.object({
|
|
|
25
26
|
enabled: z.boolean().default(true),
|
|
26
27
|
thresholdRatio: z.number().default(90),
|
|
27
28
|
scopePreset: z.boolean().default(false),
|
|
28
|
-
presets: z.string().default("
|
|
29
|
+
presets: z.string().default(""),
|
|
29
30
|
templateFile: z.string().default(""),
|
|
30
31
|
openTemplate: z.string().default(""),
|
|
31
32
|
displayStyle: z.string().default("orbit"),
|
|
@@ -194,28 +195,38 @@ async function runScope(action) {
|
|
|
194
195
|
return { ok: false, message };
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Hand compaction back where the old mechanism left it.
|
|
200
|
+
*
|
|
201
|
+
* Up to 0.2.x the takeover was written into the user's own preset files plus a
|
|
202
|
+
* root-level guard in the profile patch. Those rows now live inside this
|
|
203
|
+
* package, so a leftover switch would mount two providers in the same realm and
|
|
204
|
+
* DSH would refuse to start. Detection goes through the script's own read-only
|
|
205
|
+
* report rather than a second YAML parser; the revert runs once and is recorded
|
|
206
|
+
* so later boots skip the subprocess entirely.
|
|
207
|
+
*/
|
|
208
|
+
async function revertLegacyTakeover(ctx) {
|
|
209
|
+
const settings = readSettings();
|
|
210
|
+
if (settings?.legacyRevertedAt !== undefined) return { reverted: false, skipped: "already" };
|
|
211
|
+
const state = await runScope("state");
|
|
212
|
+
if (!state.ok) return { reverted: false, reason: state.message };
|
|
213
|
+
const line = String(state.output ?? "").split(/\r?\n/).filter(Boolean).pop() ?? "";
|
|
214
|
+
let value;
|
|
202
215
|
try {
|
|
203
|
-
|
|
204
|
-
} catch
|
|
205
|
-
|
|
206
|
-
trace("scope state failed: " + message + " | " + line.slice(0, 200));
|
|
207
|
-
return { ok: false, message };
|
|
216
|
+
value = JSON.parse(line);
|
|
217
|
+
} catch {
|
|
218
|
+
return { reverted: false, reason: "unreadable legacy state" };
|
|
208
219
|
}
|
|
209
|
-
|
|
220
|
+
const legacy = Number(value?.taken) > 0 || value?.rootGuard === "off";
|
|
221
|
+
if (!legacy) return { reverted: false, legacy: false };
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
: { ok: false, message: result.message };
|
|
223
|
+
const disabled = await runScope("disable");
|
|
224
|
+
if (!disabled.ok) return { reverted: false, reason: disabled.message };
|
|
225
|
+
|
|
226
|
+
writeSettings(ctx, { ...settings, legacyRevertedAt: new Date().toISOString() });
|
|
227
|
+
trace(`legacy takeover reverted: taken=${value?.taken}/${value?.total} rootGuard=${value?.rootGuard}`);
|
|
228
|
+
ctx.logger?.info?.("[handoff-ui] 旧版接管残留已恢复为原生压缩(预设文件与根级守卫)");
|
|
229
|
+
return { reverted: true, taken: value?.taken, total: value?.total };
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
const CHANNEL = "/dsh-auto-compaction";
|
|
@@ -248,6 +259,22 @@ function sanitize(payload) {
|
|
|
248
259
|
export function apply(ctx, config = {}) {
|
|
249
260
|
const seeded = normalize(config);
|
|
250
261
|
if (readSettings() === undefined) writeSettings(ctx, seeded);
|
|
262
|
+
// Takeover rows are regenerated from the live preset definitions on every
|
|
263
|
+
// load, into this package's own patch file. Nothing here depends on the
|
|
264
|
+
// user's scope selection: an unselected preset is handled by the engine
|
|
265
|
+
// delegating to the stock one, so installing is enough to take over and
|
|
266
|
+
// uninstalling is enough to hand compaction back.
|
|
267
|
+
void (async () => {
|
|
268
|
+
const legacy = await revertLegacyTakeover(ctx);
|
|
269
|
+
if (legacy.reverted) trace("legacy takeover reverted before mounting");
|
|
270
|
+
else if (legacy.reason) trace("legacy takeover check skipped: " + legacy.reason);
|
|
271
|
+
return mountTakeover(ctx);
|
|
272
|
+
})().then((result) => {
|
|
273
|
+
trace(`takeover mount ${result.ok ? "ok" : "failed"} taken=${result.taken?.join(",") ?? "-"} mounted=${result.mounted?.join(",") ?? "-"} skipped=${result.skipped?.join(",") ?? "-"} ${result.reason ?? ""}`);
|
|
274
|
+
}).catch((error) => {
|
|
275
|
+
trace("takeover mount threw: " + (error instanceof Error ? error.message : String(error)));
|
|
276
|
+
ctx.logger?.warn?.(`[handoff-ui] takeover mount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
277
|
+
});
|
|
251
278
|
void handleOpen(ctx, config.openTemplate, seeded.templateFile || TEMPLATE_FILE).catch((error) => {
|
|
252
279
|
ctx.logger?.warn?.(`[handoff-ui] openTemplate failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
280
|
});
|
|
@@ -283,17 +310,29 @@ export function apply(ctx, config = {}) {
|
|
|
283
310
|
const next = sanitize(payload);
|
|
284
311
|
trace("set " + JSON.stringify(next));
|
|
285
312
|
writeSettings(ctx, next);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
return { ok: true, value: { ...next, switchApplied: true, defaultTemplate: TEMPLATE_FILE } };
|
|
313
|
+
// No rewriting of preset files: the engine applies the switch and
|
|
314
|
+
// the scope per preset at decision time, so a save is live at the
|
|
315
|
+
// next idle check and uninstalling still leaves nothing behind.
|
|
316
|
+
return { ok: true, value: { ...next, defaultTemplate: TEMPLATE_FILE } };
|
|
291
317
|
}
|
|
292
318
|
if (endpoint === "scope") {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
319
|
+
const stored = readSettings() ?? seeded;
|
|
320
|
+
try {
|
|
321
|
+
const inventory = await presetInventory(ctx, stored);
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
value: {
|
|
325
|
+
enabled: stored.enabled !== false,
|
|
326
|
+
scope: stored.scope === "preset" ? "preset" : "global",
|
|
327
|
+
source: "bundle-patch",
|
|
328
|
+
...inventory,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
trace("scope inventory failed: " + message);
|
|
334
|
+
return { ok: false, error: { code: "auto-compaction/scope-unknown", message } };
|
|
335
|
+
}
|
|
297
336
|
}
|
|
298
337
|
if (endpoint === "open") {
|
|
299
338
|
trace("open " + JSON.stringify(payload));
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount the takeover, in this package's own patch file.
|
|
3
|
+
*
|
|
4
|
+
* The rows that hand compaction over to this plugin live in the bundle patch
|
|
5
|
+
* shipped inside the package, because that is the only place uninstalling
|
|
6
|
+
* removes them automatically: DSH's uninstall neither clears nor refuses
|
|
7
|
+
* config-replacement rows in a user's own patch, so writing them there would
|
|
8
|
+
* leave a preset pointing at a package that no longer exists — a broken
|
|
9
|
+
* compaction setup the user cannot see. Writing here costs nothing on removal.
|
|
10
|
+
*
|
|
11
|
+
* Rows are generated from the live preset definitions rather than baked in, so
|
|
12
|
+
* a preset that gains a tool in a later DSH release keeps it. The generator
|
|
13
|
+
* lives in the engine package and is loaded lazily: DSH's host module fallback
|
|
14
|
+
* cannot resolve a linked package's subpath from here, and a top-level import
|
|
15
|
+
* blocks startup.
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
/** The patch file this package is mounted from. */
|
|
24
|
+
function patchFile() {
|
|
25
|
+
const override = process.env.DSH_HANDOFF_TAKEOVER_FILE;
|
|
26
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
27
|
+
return join(dirname(requireFromHere.resolve("../package.json")), "cordis.patch.yml");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load the row generator without importing the engine package statically.
|
|
32
|
+
* @returns the module, or undefined when it cannot be resolved.
|
|
33
|
+
*/
|
|
34
|
+
async function loadGenerator() {
|
|
35
|
+
// Dynamic import rather than createRequire: the generator is ESM. Installed,
|
|
36
|
+
// the engine sits beside this package in the same node_modules and publishes
|
|
37
|
+
// ./lib/*; in development it is the monorepo sibling, since the workspace
|
|
38
|
+
// installs nothing and everything runs through junctions.
|
|
39
|
+
for (const attempt of [
|
|
40
|
+
() => import("dsh-auto-compaction-engine/lib/takeover-rows.js"),
|
|
41
|
+
() => import("../../dsh-auto-compaction-engine/lib/takeover-rows.js"),
|
|
42
|
+
]) {
|
|
43
|
+
try {
|
|
44
|
+
return await attempt();
|
|
45
|
+
} catch { /* try the next location */ }
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The profile manifest DSH composes its layers from. */
|
|
51
|
+
function profileManifest() {
|
|
52
|
+
const override = process.env.DSH_PROFILE_MANIFEST;
|
|
53
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
54
|
+
const appData = process.env.APP_DATA
|
|
55
|
+
?? path.join(process.env.SystemDrive ?? "C:", "Users", process.env.USERNAME ?? process.env.USER ?? "", "AppData", "Roaming");
|
|
56
|
+
return path.join(appData, "dsh-desktop", "harness", "profiles", "web", "package.json");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Keep this package's patch layer behind every preset declaration.
|
|
61
|
+
*
|
|
62
|
+
* Layer order is the order of `dsh.profile.bundles`, and a takeover row is an
|
|
63
|
+
* update of a preset row that has to exist by the time our layer applies:
|
|
64
|
+
* presets declared by a later layer would leave our row with nothing to point
|
|
65
|
+
* at, silently. DSH's own presets come early, but a preset installed from the
|
|
66
|
+
* market or written by the user can come after us, so the entry belongs last.
|
|
67
|
+
*
|
|
68
|
+
* The desktop market appends newly installed bundles to the end, so this is a
|
|
69
|
+
* no-op for a fresh install; it matters when another plugin is installed after
|
|
70
|
+
* us. Verified on this machine: without the entry 4 of 6 presets mounted, with
|
|
71
|
+
* it at the end all 6 did.
|
|
72
|
+
*
|
|
73
|
+
* Text surgery rather than a JSON rewrite, because this file is the user's and
|
|
74
|
+
* the market rewrites it too.
|
|
75
|
+
*/
|
|
76
|
+
export function ensureBundleLayer(text, name = "dsh-auto-compaction") {
|
|
77
|
+
if (typeof text !== "string" || text.length === 0) return { text, changed: false, reason: "manifest unreadable" };
|
|
78
|
+
const start = text.indexOf("\"bundles\"");
|
|
79
|
+
const open = start < 0 ? -1 : text.indexOf("[", start);
|
|
80
|
+
const close = open < 0 ? -1 : text.indexOf("]", open);
|
|
81
|
+
if (close < 0) return { text, changed: false, reason: "no dsh.profile.bundles array" };
|
|
82
|
+
const body = text.slice(open + 1, close);
|
|
83
|
+
const entries = [...body.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
84
|
+
if (entries.length > 0 && entries[entries.length - 1] === name) return { text, changed: false, reason: "" };
|
|
85
|
+
|
|
86
|
+
const kept = entries.filter((entry) => entry !== name);
|
|
87
|
+
const rebuilt = kept.length === 0 ? `["${name}"]` : `[\n${kept.concat([name]).map((entry) => ` "${entry}"`).join(",\n")}\n ]`;
|
|
88
|
+
return { text: text.slice(0, open) + rebuilt + text.slice(close + 1), changed: true, reason: "" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Apply the layer rule to the profile manifest, writing only on change. */
|
|
92
|
+
export function ensureBundleLayerFile() {
|
|
93
|
+
const file = profileManifest();
|
|
94
|
+
let text = "";
|
|
95
|
+
try {
|
|
96
|
+
text = readFileSync(file, "utf8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { ok: false, file, reason: `manifest missing: ${error instanceof Error ? error.message : String(error)}` };
|
|
99
|
+
}
|
|
100
|
+
const result = ensureBundleLayer(text);
|
|
101
|
+
if (!result.changed) return { ok: true, file, changed: false, reason: result.reason };
|
|
102
|
+
try {
|
|
103
|
+
JSON.parse(result.text);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return { ok: false, file, reason: `refusing to write invalid JSON: ${error.message}` };
|
|
106
|
+
}
|
|
107
|
+
const tmp = `${file}.tmp`;
|
|
108
|
+
writeFileSync(tmp, result.text, "utf8");
|
|
109
|
+
renameSync(tmp, file);
|
|
110
|
+
return { ok: true, file, changed: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ask DSH which agent presets exist and what each one currently declares.
|
|
115
|
+
*
|
|
116
|
+
* `list()` publishes the display metadata ({id, name, description, order,
|
|
117
|
+
* broken?}) and `readDocument()` the plugin list as a YAML string; a preset's
|
|
118
|
+
* config is exactly `{id, name, description, order, plugins}`, so the two
|
|
119
|
+
* together are the whole definition and nothing here is guessed.
|
|
120
|
+
*/
|
|
121
|
+
async function readPresets(ctx, generator) {
|
|
122
|
+
let service;
|
|
123
|
+
try {
|
|
124
|
+
service = ctx.get?.("agentPresets");
|
|
125
|
+
} catch {
|
|
126
|
+
return { presets: [], failures: [], reason: "agentPresets service unavailable" };
|
|
127
|
+
}
|
|
128
|
+
if (service === undefined || typeof service.list !== "function") {
|
|
129
|
+
return { presets: [], failures: [], reason: "agentPresets service has no list()" };
|
|
130
|
+
}
|
|
131
|
+
if (generator === undefined) {
|
|
132
|
+
return { presets: [], failures: [], reason: "row generator not resolvable" };
|
|
133
|
+
}
|
|
134
|
+
let listed;
|
|
135
|
+
try {
|
|
136
|
+
listed = await service.list();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return { presets: [], failures: [], reason: `list() failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
139
|
+
}
|
|
140
|
+
const entries = Array.isArray(listed) ? listed : Array.isArray(listed?.presets) ? listed.presets : [];
|
|
141
|
+
const presets = [];
|
|
142
|
+
const failures = [];
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const id = typeof entry?.id === "string" && entry.id.length > 0 ? entry.id : undefined;
|
|
145
|
+
if (id === undefined) continue;
|
|
146
|
+
let document;
|
|
147
|
+
try {
|
|
148
|
+
document = typeof service.readDocument === "function" ? await service.readDocument(id) : undefined;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
failures.push(`${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const parsed = generator.parsePlugins(document?.content);
|
|
154
|
+
if (parsed.plugins === undefined) {
|
|
155
|
+
failures.push(`${id}: ${parsed.reason}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const config = { id };
|
|
159
|
+
for (const key of ["name", "description", "order"]) {
|
|
160
|
+
if (entry[key] !== undefined) config[key] = entry[key];
|
|
161
|
+
}
|
|
162
|
+
config.plugins = parsed.plugins;
|
|
163
|
+
presets.push({
|
|
164
|
+
id,
|
|
165
|
+
config,
|
|
166
|
+
label: typeof entry.name === "string" && entry.name.length > 0 ? entry.name : id,
|
|
167
|
+
// What the composition actually registers, and whether it failed to.
|
|
168
|
+
provider: generator.compactionProvider?.(config)?.name ?? "",
|
|
169
|
+
broken: typeof entry.broken === "string" ? entry.broken : "",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return { presets, failures };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Write the managed block, atomically, only when it actually changed. */
|
|
176
|
+
function writeBlock(text, file) {
|
|
177
|
+
const tmp = `${file}.tmp`;
|
|
178
|
+
writeFileSync(tmp, text, "utf8");
|
|
179
|
+
renameSync(tmp, file);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Regenerate the takeover rows at activation.
|
|
184
|
+
*
|
|
185
|
+
* @returns {ok, file, taken, skipped, reason} for the log and the settings page.
|
|
186
|
+
*/
|
|
187
|
+
export async function mountTakeover(ctx) {
|
|
188
|
+
const generator = await loadGenerator();
|
|
189
|
+
if (generator === undefined) {
|
|
190
|
+
return { ok: false, reason: "row generator not resolvable", taken: [], skipped: [] };
|
|
191
|
+
}
|
|
192
|
+
const file = patchFile();
|
|
193
|
+
let current = "";
|
|
194
|
+
try {
|
|
195
|
+
current = readFileSync(file, "utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
current = "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { presets, failures = [], reason = "" } = await readPresets(ctx, generator);
|
|
201
|
+
if (presets.length === 0) {
|
|
202
|
+
// Nothing could be read: leave the file as it is rather than wiping the
|
|
203
|
+
// shipped rows on a transient host failure.
|
|
204
|
+
return { ok: false, file, taken: [], skipped: [], reason: reason || failures[0] || "no presets declared" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const engineName = process.env.DSH_HANDOFF_ENGINE_NAME ?? "dsh-auto-compaction-engine";
|
|
208
|
+
const layer = ensureBundleLayerFile();
|
|
209
|
+
if (layer.changed) ctx.logger?.info?.(`[handoff-ui] bundle layer moved to the end of dsh.profile.bundles`);
|
|
210
|
+
else if (layer.reason) ctx.logger?.warn?.(`[handoff-ui] bundle layer left as is: ${layer.reason}`);
|
|
211
|
+
const rows = generator.takeoverRows(presets, { engineName });
|
|
212
|
+
const taken = rows.map((row) => String(row.id).replace(/^preset-/, ""));
|
|
213
|
+
const skipped = presets.map((preset) => preset.id).filter((id) => !taken.includes(id));
|
|
214
|
+
/* Written rows and effective rows are different facts: the composition reads
|
|
215
|
+
* the patch at startup, so rows first written by this activation only take
|
|
216
|
+
* effect next boot. Reporting both is what keeps the settings page honest. */
|
|
217
|
+
const mounted = presets.filter((preset) => preset.provider === engineName).map((preset) => preset.id);
|
|
218
|
+
const broken = presets.filter((preset) => preset.broken).map((preset) => `${preset.id}: ${preset.broken}`);
|
|
219
|
+
/* Saying which presets were left out and why is the difference between an
|
|
220
|
+
* honest "not taken over" and a silent one. */
|
|
221
|
+
const skippedReasons = presets
|
|
222
|
+
.filter((preset) => !taken.includes(preset.id))
|
|
223
|
+
.map((preset) => {
|
|
224
|
+
const unsupported = generator.findUnsupported?.(preset.config ?? {});
|
|
225
|
+
return `${preset.id}: ${unsupported ? `含无法原样重写的值(${unsupported})` : "没有可接管的 compaction 组"}`;
|
|
226
|
+
});
|
|
227
|
+
const notes = [];
|
|
228
|
+
if (failures.length > 0) notes.push(`unreadable: ${failures.join("; ")}`);
|
|
229
|
+
if (skippedReasons.length > 0) notes.push(`skipped: ${skippedReasons.join("; ")}`);
|
|
230
|
+
if (broken.length > 0) notes.push(`broken: ${broken.join("; ")}`);
|
|
231
|
+
const block = generator.renderBlock(rows);
|
|
232
|
+
|
|
233
|
+
// Nothing to take over and nothing recorded before: leave the shipped file
|
|
234
|
+
// alone rather than appending an empty block.
|
|
235
|
+
if (rows.length === 0 && !current.includes(generator.BLOCK_MARKERS.BEGIN)) {
|
|
236
|
+
return { ok: true, file, taken: [], skipped, mounted, broken, reason: notes.join(" | ") };
|
|
237
|
+
}
|
|
238
|
+
if (generator.needsRewrite(current, block)) {
|
|
239
|
+
try {
|
|
240
|
+
writeBlock(generator.mergeBlock(current, block), file);
|
|
241
|
+
ctx.logger?.info?.(`[handoff-ui] takeover rows written to ${file} (${taken.length} preset(s))`);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false, file, taken, skipped,
|
|
245
|
+
reason: `patch write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { ok: true, file, taken, skipped, mounted, broken, reason: notes.join(" | ") };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Which presets the shipped block currently covers. */
|
|
253
|
+
export async function readTakenPresets() {
|
|
254
|
+
const generator = await loadGenerator();
|
|
255
|
+
const file = patchFile();
|
|
256
|
+
if (generator === undefined) return [];
|
|
257
|
+
let text = "";
|
|
258
|
+
try {
|
|
259
|
+
text = readFileSync(file, "utf8");
|
|
260
|
+
} catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const { BEGIN, END } = generator.BLOCK_MARKERS;
|
|
264
|
+
const begin = text.indexOf(BEGIN);
|
|
265
|
+
const end = text.indexOf(END);
|
|
266
|
+
if (begin < 0 || end < 0 || end < begin) return [];
|
|
267
|
+
return [...text.slice(begin + BEGIN.length, end).matchAll(/^-\s+id:\s*preset-(\S+)/gm)].map((m) => m[1]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Everything the settings page needs to answer "who compacts each preset".
|
|
272
|
+
*
|
|
273
|
+
* `covered` is what the block in this package's patch reaches; `effective` adds
|
|
274
|
+
* the runtime gates the engine applies per preset (the switch and the scope).
|
|
275
|
+
*/
|
|
276
|
+
export async function presetInventory(ctx, settings = {}) {
|
|
277
|
+
const generator = await loadGenerator();
|
|
278
|
+
const covered = new Set(await readTakenPresets());
|
|
279
|
+
const { presets, failures = [], reason = "" } = await readPresets(ctx, generator);
|
|
280
|
+
const selected = new Set(Array.isArray(settings.presets) ? settings.presets : []);
|
|
281
|
+
const global = settings.scope !== "preset";
|
|
282
|
+
const enabled = settings.enabled !== false;
|
|
283
|
+
const rows = presets.map((preset) => {
|
|
284
|
+
const isCovered = covered.has(preset.id);
|
|
285
|
+
const isSelected = global ? isCovered : isCovered && selected.has(preset.id);
|
|
286
|
+
return {
|
|
287
|
+
id: preset.id,
|
|
288
|
+
label: typeof preset.label === "string" ? preset.label : undefined,
|
|
289
|
+
covered: isCovered,
|
|
290
|
+
selected: isSelected,
|
|
291
|
+
effective: isSelected && enabled,
|
|
292
|
+
// Whether this boot's composition already registers our provider; a row
|
|
293
|
+
// written during this activation only shows up here after a restart.
|
|
294
|
+
mounted: preset.provider === (process.env.DSH_HANDOFF_ENGINE_NAME ?? "dsh-auto-compaction-engine"),
|
|
295
|
+
broken: preset.broken || undefined,
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
presets: rows,
|
|
300
|
+
total: rows.length,
|
|
301
|
+
covered: rows.filter((row) => row.covered).length,
|
|
302
|
+
mounted: rows.filter((row) => row.mounted).length,
|
|
303
|
+
effective: rows.filter((row) => row.effective).length,
|
|
304
|
+
reason: reason || (failures.length > 0 ? `unreadable: ${failures.join("; ")}` : ""),
|
|
305
|
+
};
|
|
306
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-auto-compaction",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"dsh-auto-compaction-engine": "^0.2.
|
|
54
|
+
"dsh-auto-compaction-engine": "^0.2.3"
|
|
55
55
|
}
|
|
56
56
|
}
|