@goodandready/dsh-goal 0.1.3 → 0.1.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/lib/index.js CHANGED
@@ -9,6 +9,32 @@ export const inject = ['webServer', 'settings'];
9
9
 
10
10
  const NS = 'dsh-goal';
11
11
 
12
+ function sessionIdOf(invocationOrReq, fallback = 'default') {
13
+ if (!invocationOrReq) return fallback;
14
+ try {
15
+ // 1. DSH invocation / event / turn
16
+ if (invocationOrReq.sessionId) return String(invocationOrReq.sessionId);
17
+ if (invocationOrReq.session) {
18
+ return String(invocationOrReq.session.id || invocationOrReq.session.header?.id || fallback);
19
+ }
20
+ if (invocationOrReq.data?.sessionId) return String(invocationOrReq.data.sessionId);
21
+ if (invocationOrReq.agent?.session) {
22
+ return String(invocationOrReq.agent.session.id || invocationOrReq.agent.session.header?.id || fallback);
23
+ }
24
+ // 2. HTTP Request (req)
25
+ if (invocationOrReq.headers) {
26
+ const headerSid = invocationOrReq.headers['x-dsh-session-id'];
27
+ if (headerSid) return String(headerSid);
28
+ if (invocationOrReq.url) {
29
+ const url = new URL(invocationOrReq.url, 'http://localhost');
30
+ const querySid = url.searchParams.get('sessionId') || url.searchParams.get('session');
31
+ if (querySid) return String(querySid);
32
+ }
33
+ }
34
+ } catch (_) {}
35
+ return fallback;
36
+ }
37
+
12
38
  // Схема-функция: в rc.1 settings.register(NS, schema, { base }) ожидает
13
39
  // schemastery-схему вторым аргументом.
14
40
  // Issue #24: storagePath объявлен в схеме конфигурации плагина
@@ -23,6 +49,7 @@ export function apply(ctx, config = {}) {
23
49
  let settingsScope = null;
24
50
  let agentsService = null;
25
51
  const runningAgents = new Set();
52
+ const sessionAgents = new Map(); // sessionId -> agent
26
53
  let lastActiveAgent = null;
27
54
 
28
55
  let currentSettings = {
@@ -54,60 +81,85 @@ export function apply(ctx, config = {}) {
54
81
  if (status === 'running') {
55
82
  runningAgents.add(agent);
56
83
  lastActiveAgent = agent;
84
+ const sid = sessionIdOf(agent, null);
85
+ if (sid) {
86
+ sessionAgents.set(sid, agent);
87
+ }
57
88
  } else {
58
89
  runningAgents.delete(agent);
90
+ if (lastActiveAgent === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
91
+ lastActiveAgent = null;
92
+ }
93
+ const sid = sessionIdOf(agent, null);
94
+ if (sid && sessionAgents.get(sid) === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
95
+ sessionAgents.delete(sid);
96
+ }
59
97
  }
60
98
  });
61
99
 
62
- const stopRunningAgents = () => {
63
- // 1. Отмена всех агентов, находящихся в активном выполнении
64
- for (const ag of runningAgents) {
100
+ const stopRunningAgents = (sessionId = 'default') => {
101
+ const sid = sessionId || 'default';
102
+ const specificAgent = sessionAgents.get(sid);
103
+ if (specificAgent && typeof specificAgent.cancel === 'function') {
65
104
  try {
66
- if (typeof ag.cancel === 'function') {
67
- ag.cancel({ kind: 'user' });
68
- }
105
+ specificAgent.cancel({ kind: 'user' });
69
106
  } catch (err) {
70
- console.warn('[dsh-goal] Failed to cancel running agent:', err);
107
+ console.warn('[dsh-goal] Failed to cancel specific agent for session:', err);
71
108
  }
72
109
  }
73
- runningAgents.clear();
74
110
 
75
- // 2. Отмена последнего известного агента
76
- if (lastActiveAgent && lastActiveAgent.status === 'running' && typeof lastActiveAgent.cancel === 'function') {
111
+ // Отмена всех подходящих агентов в runningAgents
112
+ for (const ag of runningAgents) {
77
113
  try {
78
- lastActiveAgent.cancel({ kind: 'user' });
114
+ const agSid = sessionIdOf(ag, null);
115
+ if (!agSid || agSid === sid || sid === 'default') {
116
+ if (typeof ag.cancel === 'function') {
117
+ ag.cancel({ kind: 'user' });
118
+ }
119
+ }
79
120
  } catch (err) {
80
- console.warn('[dsh-goal] Failed to cancel lastActiveAgent:', err);
121
+ console.warn('[dsh-goal] Failed to cancel running agent:', err);
81
122
  }
82
123
  }
83
124
 
84
- // 3. Дополнительная проверка через agentsService.list()
85
- if (agentsService && typeof agentsService.list === 'function') {
86
- try {
87
- for (const ag of agentsService.list()) {
88
- if (ag && ag.status === 'running' && typeof ag.cancel === 'function') {
89
- try {
90
- ag.cancel({ kind: 'user' });
91
- } catch (_) {}
92
- }
125
+ if (lastActiveAgent && lastActiveAgent.status === 'running' && typeof lastActiveAgent.cancel === 'function') {
126
+ const lastSid = sessionIdOf(lastActiveAgent, null);
127
+ if (!lastSid || lastSid === sid || sid === 'default') {
128
+ try {
129
+ lastActiveAgent.cancel({ kind: 'user' });
130
+ } catch (err) {
131
+ console.warn('[dsh-goal] Failed to cancel lastActiveAgent:', err);
93
132
  }
94
- } catch (err) {
95
- console.warn('[dsh-goal] Failed to cancel agents from list:', err);
96
133
  }
97
134
  }
98
135
  };
99
136
 
100
- const resumeActiveAgent = (promptText) => {
137
+ const resumeActiveAgent = (promptText, sessionId = 'default') => {
138
+ const sid = sessionId || 'default';
101
139
  const resumeMsg = createGoalUserMessage(
102
140
  promptText || '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.',
103
141
  );
104
- let target = null;
105
- if (lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
106
- target = lastActiveAgent;
107
- } else if (agentsService && typeof agentsService.list === 'function') {
142
+ let target = sessionAgents.get(sid);
143
+ if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
144
+ const lastSid = sessionIdOf(lastActiveAgent, null);
145
+ if (!lastSid || lastSid === sid || sid === 'default') {
146
+ target = lastActiveAgent;
147
+ }
148
+ }
149
+ if (!target && agentsService && typeof agentsService.list === 'function') {
108
150
  const list = agentsService.list();
109
151
  if (list && list.length > 0) {
110
- target = list[list.length - 1];
152
+ for (let i = list.length - 1; i >= 0; i--) {
153
+ const cand = list[i];
154
+ const candSid = sessionIdOf(cand, null);
155
+ if (candSid === sid) {
156
+ target = cand;
157
+ break;
158
+ }
159
+ }
160
+ if (!target && sid === 'default') {
161
+ target = list[list.length - 1];
162
+ }
111
163
  }
112
164
  }
113
165
 
@@ -115,6 +167,7 @@ export function apply(ctx, config = {}) {
115
167
  try {
116
168
  target.followup(resumeMsg);
117
169
  lastActiveAgent = target;
170
+ sessionAgents.set(sid, target);
118
171
  return true;
119
172
  } catch (err) {
120
173
  console.warn('[dsh-goal] Failed to resume agent:', err);
@@ -197,12 +250,14 @@ export function apply(ctx, config = {}) {
197
250
  description: 'Управление режимом цели (Goal Mode): активация, вехи, пауза, сброс',
198
251
  input: { hint: '[<цель>|clear|pause|resume]' },
199
252
  handler: async (invocation) => {
253
+ const sid = sessionIdOf(invocation, 'default');
200
254
  if (invocation?.agent) {
201
255
  lastActiveAgent = invocation.agent;
256
+ sessionAgents.set(sid, invocation.agent);
202
257
  }
203
258
  const parsed = parseGoalInput(invocation?.rawInput);
204
259
  const liveConfig = getConfig();
205
- return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent);
260
+ return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
206
261
  },
207
262
  });
208
263
 
@@ -225,7 +280,10 @@ export function apply(ctx, config = {}) {
225
280
  const unregisterSection = pctx.systemPrompt.section({
226
281
  name: 'tool:dsh-goal',
227
282
  order,
228
- text: () => engine.getStatePromptInjection(),
283
+ text: (sessionCtx) => {
284
+ const sid = sessionIdOf(sessionCtx, 'default');
285
+ return engine.getStatePromptInjection(sid);
286
+ },
229
287
  });
230
288
 
231
289
  if (typeof unregisterSection === 'function') {
@@ -256,15 +314,16 @@ export function apply(ctx, config = {}) {
256
314
  },
257
315
  required: ['milestones'],
258
316
  },
259
- handler: async ({ milestones }) => {
260
- const snap = engine.getSnapshot();
317
+ handler: async ({ milestones }, toolCtx) => {
318
+ const sid = sessionIdOf(toolCtx, 'default');
319
+ const snap = engine.getSnapshot(sid);
261
320
  if (!snap.hasActiveGoal) {
262
321
  return { error: 'No active goal currently set. Start a goal first.' };
263
322
  }
264
- engine.addMilestones(milestones);
323
+ engine.addMilestones(milestones, true, sid);
265
324
  return {
266
325
  success: true,
267
- milestones: engine.getSnapshot().milestones,
326
+ milestones: engine.getSnapshot(sid).milestones,
268
327
  };
269
328
  },
270
329
  });
@@ -292,14 +351,15 @@ export function apply(ctx, config = {}) {
292
351
  },
293
352
  required: ['milestone_id', 'status'],
294
353
  },
295
- handler: async ({ milestone_id, status, notes }) => {
296
- const ok = engine.updateMilestone(milestone_id, status, notes);
354
+ handler: async ({ milestone_id, status, notes }, toolCtx) => {
355
+ const sid = sessionIdOf(toolCtx, 'default');
356
+ const ok = engine.updateMilestone(milestone_id, status, notes, sid);
297
357
  if (!ok) {
298
358
  return { error: `Milestone ${milestone_id} not found or no active goal.` };
299
359
  }
300
360
  return {
301
361
  success: true,
302
- snapshot: engine.getSnapshot(),
362
+ snapshot: engine.getSnapshot(sid),
303
363
  };
304
364
  },
305
365
  });
@@ -318,8 +378,9 @@ export function apply(ctx, config = {}) {
318
378
  },
319
379
  required: ['summary'],
320
380
  },
321
- handler: async ({ summary }) => {
322
- const snap = engine.completeGoal(summary);
381
+ handler: async ({ summary }, toolCtx) => {
382
+ const sid = sessionIdOf(toolCtx, 'default');
383
+ const snap = engine.completeGoal(summary, sid);
323
384
  return {
324
385
  success: true,
325
386
  completed: true,
@@ -344,8 +405,9 @@ export function apply(ctx, config = {}) {
344
405
 
345
406
  // GET /dsh-goal/state
346
407
  if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
408
+ const sid = sessionIdOf(req, 'default');
347
409
  res.statusCode = 200;
348
- return res.end(JSON.stringify(engine.getSnapshot()));
410
+ return res.end(JSON.stringify(engine.getSnapshot(sid)));
349
411
  }
350
412
 
351
413
  // POST /dsh-goal/action
@@ -373,10 +435,26 @@ export function apply(ctx, config = {}) {
373
435
  }
374
436
 
375
437
  let body = '';
376
- req.on('data', (chunk) => { body += chunk; });
438
+ let bodySize = 0;
439
+ const MAX_PAYLOAD_BYTES = 256 * 1024;
440
+ let limitExceeded = false;
441
+
442
+ req.on('data', (chunk) => {
443
+ bodySize += chunk.length;
444
+ if (bodySize > MAX_PAYLOAD_BYTES) {
445
+ limitExceeded = true;
446
+ req.pause();
447
+ res.statusCode = 413;
448
+ return res.end(JSON.stringify({ error: 'Payload too large: max 256 KB allowed' }));
449
+ }
450
+ body += chunk;
451
+ });
452
+
377
453
  req.on('end', () => {
454
+ if (limitExceeded) return;
378
455
  try {
379
456
  const data = JSON.parse(body || '{}');
457
+ const sid = data.sessionId || sessionIdOf(req, 'default');
380
458
  const { action, title, description, reason, milestoneId, status, notes } = data;
381
459
 
382
460
  let result = null;
@@ -385,27 +463,32 @@ export function apply(ctx, config = {}) {
385
463
  result = engine.startGoal(title || 'Новая цель', {
386
464
  description,
387
465
  maxIterations: getConfig().maxIterations,
388
- });
466
+ }, sid);
389
467
  break;
390
468
  case 'pause':
391
- result = engine.pause(reason || 'Пауза по кнопке интерфейса');
392
- stopRunningAgents();
469
+ result = engine.pause(reason || 'Пауза по кнопке интерфейса', sid);
470
+ stopRunningAgents(sid);
393
471
  break;
394
472
  case 'resume':
395
- result = engine.resume();
396
- resumeActiveAgent();
473
+ result = engine.resume(sid);
474
+ resumeActiveAgent(undefined, sid);
397
475
  break;
398
476
  case 'cancel':
399
- result = engine.cancel(reason || 'Отмена цели');
400
- stopRunningAgents();
477
+ result = engine.cancel(reason || 'Отмена цели', sid);
478
+ stopRunningAgents(sid);
401
479
  break;
402
480
  case 'clear':
403
- result = engine.clear();
404
- stopRunningAgents();
481
+ result = engine.clear(sid);
482
+ stopRunningAgents(sid);
483
+ sessionAgents.delete(sid);
405
484
  break;
406
485
  case 'update_milestone':
407
- engine.updateMilestone(milestoneId, status, notes);
408
- result = engine.getSnapshot();
486
+ if (!milestoneId || !status) {
487
+ res.statusCode = 400;
488
+ return res.end(JSON.stringify({ error: 'milestoneId and status are required' }));
489
+ }
490
+ engine.updateMilestone(milestoneId, status, notes, sid);
491
+ result = engine.getSnapshot(sid);
409
492
  break;
410
493
  default:
411
494
  res.statusCode = 400;
@@ -413,7 +496,7 @@ export function apply(ctx, config = {}) {
413
496
  }
414
497
 
415
498
  res.statusCode = 200;
416
- return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot() }));
499
+ return res.end(JSON.stringify({ ok: true, state: result || engine.getSnapshot(sid) }));
417
500
  } catch (parseErr) {
418
501
  res.statusCode = 400;
419
502
  return res.end(JSON.stringify({ error: parseErr.message }));
@@ -439,42 +522,63 @@ export function apply(ctx, config = {}) {
439
522
  const liveConfig = getConfig();
440
523
  if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
441
524
 
442
- const snap = engine.getSnapshot();
525
+ const sid = sessionIdOf(turn, 'default');
526
+ const snap = engine.getSnapshot(sid);
443
527
  if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
444
- const canContinue = engine.incrementIteration();
528
+ // Проверяем причину завершения хода
529
+ if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
530
+ return;
531
+ }
532
+
533
+ const canContinue = engine.incrementIteration(sid);
445
534
  if (!canContinue) return;
446
535
 
447
- // Если turn не был прерван пользователем и цель ещё активна, отправляем следующий ход
448
- if (turn?.reason?.kind !== 'aborted') {
449
- setTimeout(() => {
450
- const currentSnap = engine.getSnapshot();
451
- if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
452
- const promptMsg = createGoalUserMessage(
453
- 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.',
454
- );
455
- let target = lastActiveAgent;
456
- if (!target && agentsService && typeof agentsService.list === 'function') {
457
- const list = agentsService.list();
458
- if (list && list.length > 0) target = list[list.length - 1];
536
+ setTimeout(() => {
537
+ const currentSnap = engine.getSnapshot(sid);
538
+ if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
539
+ const promptMsg = createGoalUserMessage(
540
+ 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.',
541
+ );
542
+ let target = sessionAgents.get(sid);
543
+ if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
544
+ const lastSid = sessionIdOf(lastActiveAgent, null);
545
+ if (!lastSid || lastSid === sid || sid === 'default') {
546
+ target = lastActiveAgent;
459
547
  }
460
- if (target && typeof target.followup === 'function') {
461
- try {
462
- target.followup(promptMsg);
463
- } catch (err) {
464
- console.warn('[dsh-goal] Failed auto-drive followup:', err);
548
+ }
549
+ if (!target && agentsService && typeof agentsService.list === 'function') {
550
+ const list = agentsService.list();
551
+ if (list && list.length > 0) {
552
+ for (let i = list.length - 1; i >= 0; i--) {
553
+ const cand = list[i];
554
+ if (sessionIdOf(cand, null) === sid) {
555
+ target = cand;
556
+ break;
557
+ }
465
558
  }
559
+ if (!target && sid === 'default') target = list[list.length - 1];
466
560
  }
467
561
  }
468
- }, 300);
469
- }
562
+
563
+ if (target && typeof target.followup === 'function' && target.status !== 'stopped') {
564
+ try {
565
+ target.followup(promptMsg);
566
+ sessionAgents.set(sid, target);
567
+ } catch (err) {
568
+ console.warn('[dsh-goal] Failed auto-drive followup:', err);
569
+ }
570
+ }
571
+ }
572
+ }, 300);
470
573
  }
471
574
  };
472
575
 
473
576
  // Подписка на запрос подтверждения (approval/asked) -> автоматическая пауза
474
- const onApprovalAsked = () => {
475
- const snap = engine.getSnapshot();
577
+ const onApprovalAsked = (event) => {
578
+ const sid = sessionIdOf(event, 'default');
579
+ const snap = engine.getSnapshot(sid);
476
580
  if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
477
- engine.pause('Ожидание подтверждения действия оператором');
581
+ engine.pause('Ожидание подтверждения действия оператором', sid);
478
582
  }
479
583
  };
480
584
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Goal mode & autonomous execution plugin for DeepSeek Harness with sticky top banner",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",