@goodandready/dsh-goal 0.1.6 → 0.1.7
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/docs/design/DESIGN.md +6 -0
- package/lib/client.js +47 -9
- package/lib/command-handler.js +193 -192
- package/lib/goal-engine.js +4 -0
- package/lib/index.js +21 -5
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -90,3 +90,9 @@
|
|
|
90
90
|
- **Crash Hydration**: При перезапуске DSH цели, оставшиеся в статусе `RUNNING`, автоматически гидрируются в `PAUSED` с отметкой времени и пояснением оператору для ручного возобновления в 1 клик.
|
|
91
91
|
- **Web Audio Синтез**: Синтезированные пентатонические колокольчики через `window.AudioContext` при завершении цели (`COMPLETED` — мажорный аккорд, `FAILED` — минорный аккорд) с соблюдением настройки `enableSound`.
|
|
92
92
|
- **Milestone Integrity**: Статус выполнения вех строго контролируется агентом; чекбоксы ручной отметки в UI исключены в соответствии с архитектурным контрактом.
|
|
93
|
+
|
|
94
|
+
8. **2026-09-12 — Сессионная изоляция /goal, устойчивость SSE и строгая REST валидация (v0.1.7)**:
|
|
95
|
+
- **Сквозная сессионная изоляция в слеш-команде**: В `command-handler.js` метод `executeGoalSlashCommand` пробрасывает `sessionId` во все вызовы `engine` (`getSnapshot`, `startGoal`, `pause`, `resume`, `clear`), исключая мутацию глобальной сессии при работе в конкретном чате.
|
|
96
|
+
- **Устойчивость SSE с Exponential Backoff**: На клиенте внедрена автоматическая схема повторного подключения `EventSource` с возрастающей задержкой (от 2s до 30s) и рандомизированным джиттером для защиты от шторма переподключений.
|
|
97
|
+
- **Строгая REST валидация и Enum Guard**: Обработчик `POST /dsh-goal/action` валидирует непустой `title` при старте цели и проверяет допустимость статуса вехи по `MilestoneStatus` enum, возвращая внятный HTTP 400 Bad Request / 404 Not Found.
|
|
98
|
+
- **Безопасность файловой системы**: `GoalEngine.writeStateToDiskSync()` гарантированно создаёт отсутствующие родительские директории через `fs.mkdirSync(dir, { recursive: true })` перед созданием временного файла и атомарным переименованием.
|
package/lib/client.js
CHANGED
|
@@ -869,8 +869,10 @@ window.__ModuleLoader__.load({
|
|
|
869
869
|
useEffect(() => {
|
|
870
870
|
let es = null;
|
|
871
871
|
let timer = null;
|
|
872
|
+
let reconnectTimer = null;
|
|
872
873
|
let isDisposed = false;
|
|
873
874
|
let sseActive = false;
|
|
875
|
+
let retryCount = 0;
|
|
874
876
|
|
|
875
877
|
const scheduleNextPoll = () => {
|
|
876
878
|
if (isDisposed || sseActive) return;
|
|
@@ -891,18 +893,33 @@ window.__ModuleLoader__.load({
|
|
|
891
893
|
}, delay);
|
|
892
894
|
};
|
|
893
895
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
+
const connectSSE = () => {
|
|
897
|
+
if (isDisposed) return;
|
|
898
|
+
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') {
|
|
899
|
+
scheduleNextPoll();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
896
903
|
try {
|
|
904
|
+
if (es) {
|
|
905
|
+
es.close();
|
|
906
|
+
es = null;
|
|
907
|
+
}
|
|
908
|
+
|
|
897
909
|
const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
|
|
898
910
|
es = new window.EventSource(`/dsh-goal/events${query}`);
|
|
899
911
|
|
|
900
912
|
es.onopen = () => {
|
|
901
913
|
sseActive = true;
|
|
914
|
+
retryCount = 0;
|
|
902
915
|
if (timer) {
|
|
903
916
|
clearTimeout(timer);
|
|
904
917
|
timer = null;
|
|
905
918
|
}
|
|
919
|
+
if (reconnectTimer) {
|
|
920
|
+
clearTimeout(reconnectTimer);
|
|
921
|
+
reconnectTimer = null;
|
|
922
|
+
}
|
|
906
923
|
};
|
|
907
924
|
|
|
908
925
|
es.onmessage = (event) => {
|
|
@@ -916,22 +933,38 @@ window.__ModuleLoader__.load({
|
|
|
916
933
|
};
|
|
917
934
|
|
|
918
935
|
es.onerror = () => {
|
|
919
|
-
// При сбое соединения SSE переключаемся на адаптивный fallback поллинг
|
|
920
936
|
sseActive = false;
|
|
921
|
-
if (
|
|
922
|
-
|
|
937
|
+
if (es) {
|
|
938
|
+
es.close();
|
|
939
|
+
es = null;
|
|
940
|
+
}
|
|
941
|
+
if (!isDisposed) {
|
|
942
|
+
// Запускаем немедленный опрос для непрерывности работы UI
|
|
943
|
+
if (!timer) scheduleNextPoll();
|
|
944
|
+
|
|
945
|
+
// Экспоненциальный backoff для повторного подключения SSE (от 2s до 30s) с джиттером
|
|
946
|
+
retryCount = Math.min(retryCount + 1, 5);
|
|
947
|
+
const backoffBase = Math.min(30000, 2000 * Math.pow(1.8, retryCount - 1));
|
|
948
|
+
const jitter = Math.random() * 1000;
|
|
949
|
+
const reconnectDelay = Math.round(backoffBase + jitter);
|
|
950
|
+
|
|
951
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
952
|
+
reconnectTimer = setTimeout(() => {
|
|
953
|
+
reconnectTimer = null;
|
|
954
|
+
connectSSE();
|
|
955
|
+
}, reconnectDelay);
|
|
923
956
|
}
|
|
924
957
|
};
|
|
925
958
|
} catch (_) {
|
|
926
959
|
sseActive = false;
|
|
927
960
|
scheduleNextPoll();
|
|
928
961
|
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
connectSSE();
|
|
932
965
|
|
|
933
966
|
// Первоначальная загрузка состояния
|
|
934
|
-
fetchState().then((
|
|
967
|
+
fetchState().then(() => {
|
|
935
968
|
if (!sseActive) scheduleNextPoll();
|
|
936
969
|
});
|
|
937
970
|
|
|
@@ -942,6 +975,10 @@ window.__ModuleLoader__.load({
|
|
|
942
975
|
fetchState().then(() => {
|
|
943
976
|
scheduleNextPoll();
|
|
944
977
|
});
|
|
978
|
+
// При возвращении на вкладку можно попытаться сразу переподключить SSE
|
|
979
|
+
if (!reconnectTimer && !sseActive) {
|
|
980
|
+
connectSSE();
|
|
981
|
+
}
|
|
945
982
|
}
|
|
946
983
|
}
|
|
947
984
|
};
|
|
@@ -957,6 +994,7 @@ window.__ModuleLoader__.load({
|
|
|
957
994
|
es = null;
|
|
958
995
|
}
|
|
959
996
|
if (timer) clearTimeout(timer);
|
|
997
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
960
998
|
if (typeof document !== 'undefined' && document.removeEventListener) {
|
|
961
999
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
962
1000
|
}
|
package/lib/command-handler.js
CHANGED
|
@@ -1,192 +1,193 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { GoalState } from './goal-engine.js';
|
|
3
|
-
|
|
4
|
-
export const USAGE = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Парсинг строки ввода для команды /goal
|
|
8
|
-
* @param {string} rawInput
|
|
9
|
-
* @returns {{ action: string, text?: string }}
|
|
10
|
-
*/
|
|
11
|
-
export function parseGoalInput(rawInput = '') {
|
|
12
|
-
const input = String(rawInput || '').trim();
|
|
13
|
-
if (!input) {
|
|
14
|
-
return { action: 'show' };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const lower = input.toLowerCase();
|
|
18
|
-
if (lower === 'clear') return { action: 'clear' };
|
|
19
|
-
if (lower === 'pause') return { action: 'pause' };
|
|
20
|
-
if (lower === 'resume') return { action: 'resume' };
|
|
21
|
-
|
|
22
|
-
return { action: 'start', text: input };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Создание валидного identified сообщения пользователя для отправки агенту через followup
|
|
27
|
-
* @param {string} text
|
|
28
|
-
* @returns {Object}
|
|
29
|
-
*/
|
|
30
|
-
export function createGoalUserMessage(text) {
|
|
31
|
-
return {
|
|
32
|
-
id: randomUUID(),
|
|
33
|
-
role: 'user',
|
|
34
|
-
content: [{ type: 'text', text }],
|
|
35
|
-
source: { kind: 'user' },
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Формирование стартового промпта с жестким требованием составить план работ
|
|
41
|
-
* @param {string} goalText
|
|
42
|
-
* @returns {string}
|
|
43
|
-
*/
|
|
44
|
-
export function formatGoalStartPrompt(goalText) {
|
|
45
|
-
return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
|
|
46
|
-
`СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
|
|
47
|
-
`1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
48
|
-
`2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
|
|
49
|
-
`3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
|
|
50
|
-
`4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Форматирование текстового ответа для UI и постановка/управление задачей агента
|
|
55
|
-
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
56
|
-
* @param {{ action: string, text?: string }} parsed
|
|
57
|
-
* @param {Object} [config]
|
|
58
|
-
* @param {Object} [agent]
|
|
59
|
-
* @
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { GoalState } from './goal-engine.js';
|
|
3
|
+
|
|
4
|
+
export const USAGE = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Парсинг строки ввода для команды /goal
|
|
8
|
+
* @param {string} rawInput
|
|
9
|
+
* @returns {{ action: string, text?: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function parseGoalInput(rawInput = '') {
|
|
12
|
+
const input = String(rawInput || '').trim();
|
|
13
|
+
if (!input) {
|
|
14
|
+
return { action: 'show' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const lower = input.toLowerCase();
|
|
18
|
+
if (lower === 'clear') return { action: 'clear' };
|
|
19
|
+
if (lower === 'pause') return { action: 'pause' };
|
|
20
|
+
if (lower === 'resume') return { action: 'resume' };
|
|
21
|
+
|
|
22
|
+
return { action: 'start', text: input };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Создание валидного identified сообщения пользователя для отправки агенту через followup
|
|
27
|
+
* @param {string} text
|
|
28
|
+
* @returns {Object}
|
|
29
|
+
*/
|
|
30
|
+
export function createGoalUserMessage(text) {
|
|
31
|
+
return {
|
|
32
|
+
id: randomUUID(),
|
|
33
|
+
role: 'user',
|
|
34
|
+
content: [{ type: 'text', text }],
|
|
35
|
+
source: { kind: 'user' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Формирование стартового промпта с жестким требованием составить план работ
|
|
41
|
+
* @param {string} goalText
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function formatGoalStartPrompt(goalText) {
|
|
45
|
+
return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
|
|
46
|
+
`СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
|
|
47
|
+
`1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
48
|
+
`2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
|
|
49
|
+
`3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
|
|
50
|
+
`4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Форматирование текстового ответа для UI и постановка/управление задачей агента
|
|
55
|
+
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
56
|
+
* @param {{ action: string, text?: string }} parsed
|
|
57
|
+
* @param {Object} [config]
|
|
58
|
+
* @param {Object} [agent]
|
|
59
|
+
* @param {string} [sessionId='default']
|
|
60
|
+
* @returns {{ kind: 'success' | 'error', text: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function executeGoalSlashCommand(engine, parsed, config = {}, agent = null, sessionId = 'default') {
|
|
63
|
+
const sid = sessionId || 'default';
|
|
64
|
+
const snap = engine.getSnapshot(sid);
|
|
65
|
+
|
|
66
|
+
switch (parsed.action) {
|
|
67
|
+
case 'show': {
|
|
68
|
+
if (!snap.hasActiveGoal) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'success',
|
|
71
|
+
text: `🎯 Режим цели (Goal Mode): цель не установлена.\n${USAGE}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const milestonesInfo = snap.milestones.length > 0
|
|
76
|
+
? `\nПлан работ:\n` + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
|
|
77
|
+
: '';
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
kind: 'success',
|
|
81
|
+
text: `🎯 Цель: «${snap.title}»\n` +
|
|
82
|
+
`Статус: ${snap.state}\n` +
|
|
83
|
+
`Время: ${snap.formattedElapsed}\n` +
|
|
84
|
+
`Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
85
|
+
milestonesInfo +
|
|
86
|
+
`\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'clear': {
|
|
91
|
+
if (!snap.hasActiveGoal) {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'success',
|
|
94
|
+
text: 'Цель не была установлена.',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
engine.clear(sid);
|
|
98
|
+
if (agent && typeof agent.cancel === 'function') {
|
|
99
|
+
try {
|
|
100
|
+
agent.cancel({ kind: 'user', reason: 'Goal cleared by user' });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.warn('[dsh-goal] Failed to cancel agent on clear:', err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
kind: 'success',
|
|
107
|
+
text: '🎯 Цель сброшена.',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case 'pause': {
|
|
112
|
+
if (!snap.hasActiveGoal) {
|
|
113
|
+
return {
|
|
114
|
+
kind: 'error',
|
|
115
|
+
text: `Нельзя приостановить: активная цель отсутствует.\n${USAGE}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (snap.state === GoalState.PAUSED) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'success',
|
|
121
|
+
text: '⏸ Цель уже на паузе.',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
engine.pause('Пауза по команде пользователя (/goal pause)', sid);
|
|
125
|
+
if (agent && typeof agent.cancel === 'function') {
|
|
126
|
+
try {
|
|
127
|
+
agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.warn('[dsh-goal] Failed to cancel agent on pause:', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
kind: 'success',
|
|
134
|
+
text: `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'resume': {
|
|
139
|
+
if (!snap.hasActiveGoal) {
|
|
140
|
+
return {
|
|
141
|
+
kind: 'error',
|
|
142
|
+
text: `Нельзя возобновить: активная цель отсутствует.\n${USAGE}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (snap.state === GoalState.RUNNING) {
|
|
146
|
+
return {
|
|
147
|
+
kind: 'success',
|
|
148
|
+
text: '▶️ Цель уже выполняется.',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
engine.resume(sid);
|
|
152
|
+
if (agent && typeof agent.followup === 'function') {
|
|
153
|
+
try {
|
|
154
|
+
const resumePrompt = `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`;
|
|
155
|
+
agent.followup(createGoalUserMessage(resumePrompt));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.warn('[dsh-goal] Failed to followup agent on resume:', err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
kind: 'success',
|
|
162
|
+
text: `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'start': {
|
|
167
|
+
const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
|
|
168
|
+
const newSnap = engine.startGoal(parsed.text, { maxIterations }, sid);
|
|
169
|
+
|
|
170
|
+
// Если команда вызвана в контексте агента, отправляем цель в очередь модели
|
|
171
|
+
if (agent && typeof agent.followup === 'function') {
|
|
172
|
+
try {
|
|
173
|
+
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text));
|
|
174
|
+
agent.followup(userMsg);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.warn('[dsh-goal] Failed to dispatch goal to agent followup:', err);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
kind: 'success',
|
|
182
|
+
text: `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
|
|
183
|
+
`Для паузы: /goal pause | Для сброса: /goal clear`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
default:
|
|
188
|
+
return {
|
|
189
|
+
kind: 'error',
|
|
190
|
+
text: `Неизвестная команда. ${USAGE}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -163,6 +163,10 @@ export class GoalEngine {
|
|
|
163
163
|
sessions: sessionsObj,
|
|
164
164
|
...(this.goals.has('default') ? this.goals.get('default') : {}),
|
|
165
165
|
};
|
|
166
|
+
const dir = path.dirname(this.storagePath);
|
|
167
|
+
if (!fs.existsSync(dir)) {
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
169
|
+
}
|
|
166
170
|
const tmp = `${this.storagePath}.tmp.${Date.now()}`;
|
|
167
171
|
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
168
172
|
fs.renameSync(tmp, this.storagePath);
|
package/lib/index.js
CHANGED
|
@@ -526,12 +526,18 @@ export function apply(ctx, config = {}) {
|
|
|
526
526
|
|
|
527
527
|
let result = null;
|
|
528
528
|
switch (action) {
|
|
529
|
-
case 'start':
|
|
530
|
-
|
|
531
|
-
|
|
529
|
+
case 'start': {
|
|
530
|
+
const cleanTitle = typeof title === 'string' ? title.trim() : '';
|
|
531
|
+
if (!cleanTitle) {
|
|
532
|
+
res.statusCode = 400;
|
|
533
|
+
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
534
|
+
}
|
|
535
|
+
result = engine.startGoal(cleanTitle, {
|
|
536
|
+
description: typeof description === 'string' ? description.trim() : '',
|
|
532
537
|
maxIterations: getConfig().maxIterations,
|
|
533
538
|
}, sid);
|
|
534
539
|
break;
|
|
540
|
+
}
|
|
535
541
|
case 'pause':
|
|
536
542
|
result = engine.pause(reason || 'Пауза по кнопке интерфейса', sid);
|
|
537
543
|
stopRunningAgents(sid);
|
|
@@ -549,14 +555,24 @@ export function apply(ctx, config = {}) {
|
|
|
549
555
|
stopRunningAgents(sid);
|
|
550
556
|
sessionAgents.delete(sid);
|
|
551
557
|
break;
|
|
552
|
-
case 'update_milestone':
|
|
558
|
+
case 'update_milestone': {
|
|
553
559
|
if (!milestoneId || !status) {
|
|
554
560
|
res.statusCode = 400;
|
|
555
561
|
return res.end(JSON.stringify({ error: 'milestoneId and status are required' }));
|
|
556
562
|
}
|
|
557
|
-
|
|
563
|
+
const validStatuses = Object.values(MilestoneStatus);
|
|
564
|
+
if (!validStatuses.includes(status)) {
|
|
565
|
+
res.statusCode = 400;
|
|
566
|
+
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
567
|
+
}
|
|
568
|
+
const ok = engine.updateMilestone(milestoneId, status, notes, sid);
|
|
569
|
+
if (!ok) {
|
|
570
|
+
res.statusCode = 404;
|
|
571
|
+
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
572
|
+
}
|
|
558
573
|
result = engine.getSnapshot(sid);
|
|
559
574
|
break;
|
|
575
|
+
}
|
|
560
576
|
default:
|
|
561
577
|
res.statusCode = 400;
|
|
562
578
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|