dsh-workplan 0.0.0-stage → 1.4.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/CHANGELOG.md +282 -0
- package/LICENSE +21 -0
- package/README.en.md +147 -0
- package/README.md +161 -2
- package/client.js +1893 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +7 -0
- package/lib/host.js +301 -0
- package/lib/plan.js +1495 -0
- package/lib/runtime.js +1755 -0
- package/lib/watchdog.js +184 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +74 -4
- package/src/client.ts +2196 -0
- package/src/globals.d.ts +14 -0
- package/src/host.ts +333 -0
- package/src/plan.ts +1591 -0
- package/src/runtime.ts +1749 -0
- package/src/watchdog.ts +207 -0
- package/tsconfig.json +36 -0
- package/vendor/chart.umd.js +14 -0
package/lib/watchdog.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout policy for one child execution.
|
|
3
|
+
*
|
|
4
|
+
* The rule this module exists to enforce: a child is stopped only when it is
|
|
5
|
+
* demonstrably not making progress, or when it exceeds an absolute ceiling.
|
|
6
|
+
*
|
|
7
|
+
* History that produced this module: `runSubtask` used to race the child
|
|
8
|
+
* against a single 300 000 ms wall-clock timer. A research child that
|
|
9
|
+
* legitimately needed six minutes was recorded as `blocked`, `markDependentsBlocked`
|
|
10
|
+
* then turned that one verdict into a whole plan reporting 0/N complete, and the
|
|
11
|
+
* dashboard offered no recovery — the plan was terminal. Across the recorded
|
|
12
|
+
* sessions that single timer produced every one of the observed child timeouts.
|
|
13
|
+
*
|
|
14
|
+
* Progress is read from the child's own session log (`ctx.sessions.get(id).seq`),
|
|
15
|
+
* which grows with every appended step, tool call and streamed delta. That is a
|
|
16
|
+
* public, non-deprecated accessor, and it is optional: when the child session
|
|
17
|
+
* cannot be observed, only the absolute ceiling applies — never a shorter idle
|
|
18
|
+
* guess, because a false stall would kill healthy work.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether one child execution has exhausted its budget.
|
|
22
|
+
*
|
|
23
|
+
* Pure function: the watchdog loop only supplies observations.
|
|
24
|
+
*
|
|
25
|
+
* @param input.now - current time in epoch milliseconds.
|
|
26
|
+
* @param input.startedAt - when the current attempt started.
|
|
27
|
+
* @param input.lastProgressAt - last time the child's session log grew.
|
|
28
|
+
* @param input.idleMs - no-progress window that means "wedged".
|
|
29
|
+
* @param input.hardMs - absolute ceiling for one attempt.
|
|
30
|
+
* @param input.progressObservable - whether the child's log can be read at all.
|
|
31
|
+
* @returns `'exhausted'` on the ceiling, `'stalled'` on an idle window, else `null`.
|
|
32
|
+
*/
|
|
33
|
+
export function pickTimeoutReason(input) {
|
|
34
|
+
const { now, startedAt, lastProgressAt, idleMs, hardMs, progressObservable } = input;
|
|
35
|
+
if (!Number.isFinite(now) || !Number.isFinite(startedAt))
|
|
36
|
+
return null;
|
|
37
|
+
if (now - startedAt >= hardMs)
|
|
38
|
+
return 'exhausted';
|
|
39
|
+
if (progressObservable && Number.isFinite(lastProgressAt) && now - lastProgressAt >= idleMs) {
|
|
40
|
+
return 'stalled';
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Render any rejection value as readable text.
|
|
46
|
+
*
|
|
47
|
+
* `String(error)` on a plain object yields `[object Object]`; one recorded
|
|
48
|
+
* subtask was blocked with exactly that string, which made the failure
|
|
49
|
+
* undiagnosable from the dashboard and the durable result message.
|
|
50
|
+
*
|
|
51
|
+
* @param value - a rejected value of unknown shape.
|
|
52
|
+
* @returns a non-empty, human-readable description.
|
|
53
|
+
*/
|
|
54
|
+
export function describeError(value) {
|
|
55
|
+
if (value === null || value === undefined)
|
|
56
|
+
return '未知失败';
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
const text = value.trim();
|
|
59
|
+
return text === '' ? '未知失败' : text;
|
|
60
|
+
}
|
|
61
|
+
if (value instanceof Error) {
|
|
62
|
+
const message = typeof value.message === 'string' ? value.message.trim() : '';
|
|
63
|
+
return message !== '' ? message : (value.name || '未知失败');
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
const record = value;
|
|
67
|
+
for (const key of ['message', 'reason', 'error', 'detail']) {
|
|
68
|
+
const field = record[key];
|
|
69
|
+
if (typeof field === 'string' && field.trim() !== '')
|
|
70
|
+
return field.trim();
|
|
71
|
+
if (field instanceof Error && field.message?.trim())
|
|
72
|
+
return field.message.trim();
|
|
73
|
+
if (field && typeof field === 'object') {
|
|
74
|
+
const nested = describeError(field);
|
|
75
|
+
if (nested !== '未知失败' && nested !== '[object Object]')
|
|
76
|
+
return nested;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const text = JSON.stringify(value);
|
|
81
|
+
if (typeof text === 'string' && text !== '' && text !== '{}')
|
|
82
|
+
return text.slice(0, 500);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Circular structures have no JSON form; fall through to the constructor name.
|
|
86
|
+
}
|
|
87
|
+
const name = value?.constructor?.name;
|
|
88
|
+
return name ? `未命名错误(${name})` : '未命名对象错误';
|
|
89
|
+
}
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Watch one child's session-log growth and reject when it stalls or runs out.
|
|
94
|
+
*
|
|
95
|
+
* @param input.getSeq - reads the child's current event sequence, or undefined when unobservable.
|
|
96
|
+
* @param input.idleMs - no-progress window.
|
|
97
|
+
* @param input.hardMs - absolute ceiling.
|
|
98
|
+
* @param input.pollMs - observation period.
|
|
99
|
+
* @param input.makeError - builds the rejection for the decided reason.
|
|
100
|
+
* @param input.onProgress - called after each observed log growth.
|
|
101
|
+
* @param input.now - clock, injectable for tests.
|
|
102
|
+
* @returns a watchdog handle.
|
|
103
|
+
*/
|
|
104
|
+
export function createProgressWatchdog(input) {
|
|
105
|
+
const clock = input.now ?? (() => Date.now());
|
|
106
|
+
const pollMs = Math.max(10, Math.floor(input.pollMs ?? 2000));
|
|
107
|
+
const startedAt = clock();
|
|
108
|
+
let lastProgress = startedAt;
|
|
109
|
+
let lastSeq = null;
|
|
110
|
+
let growth = 0;
|
|
111
|
+
let settled = false;
|
|
112
|
+
let timer = null;
|
|
113
|
+
let rejectOnce = () => { };
|
|
114
|
+
const fired = new Promise((_resolve, reject) => {
|
|
115
|
+
rejectOnce = reject;
|
|
116
|
+
});
|
|
117
|
+
// The caller may await a race in which this promise loses; keep the
|
|
118
|
+
// rejection handled so Node never reports an unhandled rejection.
|
|
119
|
+
fired.catch(() => { });
|
|
120
|
+
const finish = (reason) => {
|
|
121
|
+
if (settled)
|
|
122
|
+
return;
|
|
123
|
+
settled = true;
|
|
124
|
+
if (timer) {
|
|
125
|
+
clearInterval(timer);
|
|
126
|
+
timer = null;
|
|
127
|
+
}
|
|
128
|
+
rejectOnce(input.makeError(reason));
|
|
129
|
+
};
|
|
130
|
+
const poll = () => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
const now = clock();
|
|
134
|
+
let seq;
|
|
135
|
+
try {
|
|
136
|
+
seq = input.getSeq();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
seq = undefined;
|
|
140
|
+
}
|
|
141
|
+
if (typeof seq === 'number' && Number.isFinite(seq)) {
|
|
142
|
+
if (lastSeq === null) {
|
|
143
|
+
lastSeq = seq;
|
|
144
|
+
}
|
|
145
|
+
else if (seq > lastSeq) {
|
|
146
|
+
lastSeq = seq;
|
|
147
|
+
growth += 1;
|
|
148
|
+
lastProgress = now;
|
|
149
|
+
try {
|
|
150
|
+
input.onProgress?.({ now, seq, elapsedMs: now - startedAt });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// An observer must never break the watchdog.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const reason = pickTimeoutReason({
|
|
158
|
+
now,
|
|
159
|
+
startedAt,
|
|
160
|
+
lastProgressAt: lastProgress,
|
|
161
|
+
idleMs: Math.max(1, Math.floor(input.idleMs)),
|
|
162
|
+
hardMs: Math.max(1, Math.floor(input.hardMs)),
|
|
163
|
+
progressObservable: lastSeq !== null,
|
|
164
|
+
});
|
|
165
|
+
if (reason)
|
|
166
|
+
finish(reason);
|
|
167
|
+
};
|
|
168
|
+
timer = setInterval(poll, pollMs);
|
|
169
|
+
timer.unref?.();
|
|
170
|
+
return {
|
|
171
|
+
fired,
|
|
172
|
+
stop() {
|
|
173
|
+
if (settled)
|
|
174
|
+
return;
|
|
175
|
+
settled = true;
|
|
176
|
+
if (timer) {
|
|
177
|
+
clearInterval(timer);
|
|
178
|
+
timer = null;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
lastProgressAt: () => lastProgress,
|
|
182
|
+
progressEvents: () => growth,
|
|
183
|
+
};
|
|
184
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-workplan",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.4.3",
|
|
4
|
+
"description": "Automatic task decomposition, real department subagents, and a responsive workplan dashboard for DeepSeek Harness.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/abbaababa/dsh-workplan.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/abbaababa/dsh-workplan#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/abbaababa/dsh-workplan/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"dsh",
|
|
20
|
+
"deepseek-harness",
|
|
21
|
+
"deepseek",
|
|
22
|
+
"plugin",
|
|
23
|
+
"bundle",
|
|
24
|
+
"workplan",
|
|
25
|
+
"task-decomposition",
|
|
26
|
+
"subagents",
|
|
27
|
+
"dashboard"
|
|
28
|
+
],
|
|
29
|
+
"main": "lib/host.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./lib/host.js",
|
|
32
|
+
"./entry": "./lib/host.js",
|
|
33
|
+
"./client": "./client.js",
|
|
34
|
+
"./locale/*.json": "./locale/*.json",
|
|
35
|
+
"./icon": "./icon.svg",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"src",
|
|
40
|
+
"lib",
|
|
41
|
+
"vendor",
|
|
42
|
+
"client.js",
|
|
43
|
+
"tsconfig.json",
|
|
44
|
+
"cordis.patch.yml",
|
|
45
|
+
"locale",
|
|
46
|
+
"icon.svg",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"README.md",
|
|
49
|
+
"README.en.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22"
|
|
54
|
+
},
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"platform": "web",
|
|
61
|
+
"immediately": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"clean": "rm -rf dist dist-tests",
|
|
66
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && cp dist/plan.js dist/runtime.js dist/host.js dist/watchdog.js lib/ && cp dist/client.js client.js",
|
|
67
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
68
|
+
"test": "npm run build && rm -rf dist-tests && tsc -p tsconfig.test.json && node --test dist-tests/tests/*.test.js",
|
|
69
|
+
"verify": "npm run check && npm test"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@types/node": "^22.20.5",
|
|
73
|
+
"@types/react": "^18.3.31",
|
|
74
|
+
"typescript": "^5.9.3"
|
|
75
|
+
}
|
|
76
|
+
}
|