@goodandready/dsh-goal 0.2.12 → 0.2.14

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
@@ -1,11 +1,16 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import z from '@deepseek-ai/schemastery';
4
- import { GoalEngine, GoalState, detectLanguage, sessionIdOf } from './goal-engine.js';
5
- import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
4
+ import { AddonStore } from './addon-store.js';
5
+ import { TokenBudgetGuard } from './token-budget-guard.js';
6
+ import { GitSnapshotManager } from './git-snapshot-manager.js';
7
+ import { MilestoneManager } from './milestone-manager.js';
6
8
  import { registerRoutes } from './routes.js';
7
9
  import { registerTools } from './tools.js';
10
+ import { saveGoalReportFile } from './engine-reports.js';
8
11
  import { registerPluginUpdater } from './updater.js';
12
+ import { GoalEngine, GoalState } from './goal-engine.js';
13
+ import { sessionIdOf } from './goal-engine-constants.js';
9
14
 
10
15
  export const name = '@goodandready/dsh-goal';
11
16
  export const inject = ['webServer', 'settings'];
@@ -34,12 +39,26 @@ export const Config = z.object({
34
39
  storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
35
40
  });
36
41
 
42
+ /**
43
+ * Safely access a service from Cordis context without throwing if un-injected.
44
+ * @param {any} ctx
45
+ * @param {string} name
46
+ * @returns {any}
47
+ */
48
+ export function safeService(ctx, name) {
49
+ if (!ctx) return null;
50
+ try {
51
+ if (ctx.reflect && typeof ctx.reflect.get === "function") {
52
+ return ctx.reflect.get(name, false) || null;
53
+ }
54
+ return ctx[name] || null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
37
60
  export function apply(ctx, config = {}) {
38
61
  let settingsScope = null;
39
- let agentsService = null;
40
- const runningAgents = new Set();
41
- const sessionAgents = new Map();
42
- let lastActiveAgent = null;
43
62
  const sseClients = new Map();
44
63
 
45
64
  let currentSettings = {
@@ -72,433 +91,245 @@ export function apply(ctx, config = {}) {
72
91
  const storagePath = customStorage || (isTest ? null : path.join(defaultStorageDir, 'dsh-goal-state.json'));
73
92
  const logger = ctx.logger ? ctx.logger('goal') : console;
74
93
 
75
- const engine = new GoalEngine({
76
- defaultMaxIterations: currentSettings.maxIterations,
77
- autoDrive: currentSettings.autoDrive,
78
- enableSound: currentSettings.enableSound,
79
- showQuickLaunchButton: currentSettings.showQuickLaunchButton,
80
- consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
94
+ // Initialize addon modules
95
+ const addonStore = new AddonStore(storagePath, logger);
96
+ const gitSnapshotManager = new GitSnapshotManager(addonStore, { logger });
97
+ const milestoneManager = new MilestoneManager(addonStore, {
98
+ gitSnapshotManager,
99
+ enforceDependencies: currentSettings.enableMilestoneDependencies,
100
+ logger,
101
+ });
102
+ const tokenBudgetGuard = new TokenBudgetGuard({
81
103
  maxTokenBudget: currentSettings.maxTokenBudget,
82
- budgetWarningThreshold: currentSettings.budgetWarningThreshold,
83
- autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
84
- enableLiveActivityFeed: currentSettings.enableLiveActivityFeed,
85
- enablePreplanning: currentSettings.enablePreplanning,
86
- autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
87
- enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
88
- autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
89
- enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
90
- enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
91
- soundScheme: currentSettings.soundScheme,
92
- enableVoiceAnnouncements: currentSettings.enableVoiceAnnouncements,
93
- storagePath,
104
+ warningThreshold: currentSettings.budgetWarningThreshold,
105
+ autoScaleNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
94
106
  logger,
95
107
  });
96
108
 
97
- // Track running agents through global DSH core event
98
- ctx.inject(['agents'], (actx) => {
99
- agentsService = actx.agents;
100
- });
109
+ // Attempt migration of legacy state if present
110
+ try {
111
+ const agentsSvc = safeService(ctx, "agents");
112
+ const firstAgent = agentsSvc?.get?.(agentsSvc?.keys?.()?.next()?.value);
113
+ addonStore.migrateLegacyState(storagePath, safeService(ctx, "goals"), firstAgent);
114
+ } catch (migErr) {
115
+ logger.warn?.('[dsh-goal] Legacy migration check failed:', migErr.message);
116
+ }
101
117
 
102
- ctx.on?.('agent/status', ({ agent, status }) => {
103
- if (status === 'running') {
104
- runningAgents.add(agent);
105
- lastActiveAgent = agent;
106
- const sid = sessionIdOf(agent, null);
107
- if (sid) {
108
- sessionAgents.set(sid, agent);
109
- }
110
- } else {
111
- runningAgents.delete(agent);
112
- if (lastActiveAgent === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
113
- lastActiveAgent = null;
114
- }
115
- const sid = sessionIdOf(agent, null);
116
- if (sid && sessionAgents.get(sid) === agent && (status === 'stopped' || status === 'completed' || status === 'error')) {
117
- sessionAgents.delete(sid);
118
- }
119
- }
118
+ // Register addon tools: goal_milestones and goal_checklist
119
+ registerTools(ctx, {
120
+ milestoneManager,
121
+ goalsService: safeService(ctx, "goals"),
122
+ logger,
120
123
  });
121
124
 
122
- const stopRunningAgents = (sessionId = 'default') => {
123
- const sid = sessionId || 'default';
124
- const specificAgent = sessionAgents.get(sid);
125
- if (specificAgent && typeof specificAgent.cancel === 'function') {
126
- try {
127
- specificAgent.cancel({ kind: 'user' });
128
- } catch (err) {
129
- logger.warn('Failed to cancel specific agent for session:', err);
130
- }
131
- }
132
-
133
- for (const ag of runningAgents) {
134
- try {
135
- const agSid = sessionIdOf(ag, null);
136
- if (!agSid || agSid === sid || sid === 'default') {
137
- if (typeof ag.cancel === 'function') {
138
- ag.cancel({ kind: 'user' });
139
- }
140
- }
141
- } catch (err) {
142
- logger.warn('Failed to cancel running agent:', err);
143
- }
144
- }
125
+ // Support fallback engine for tests or non-core environments
126
+ let fallbackEngine = null;
127
+ if (!safeService(ctx, "goals")) {
128
+ fallbackEngine = new GoalEngine({
129
+ storagePath,
130
+ defaultMaxIterations: currentSettings.maxIterations,
131
+ autoDrive: currentSettings.autoDrive,
132
+ consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
133
+ maxTokenBudget: currentSettings.maxTokenBudget,
134
+ budgetWarningThreshold: currentSettings.budgetWarningThreshold,
135
+ autoCheckpointOnMilestone: currentSettings.autoCheckpointOnMilestone,
136
+ enableLiveActivityFeed: currentSettings.enableLiveActivityFeed,
137
+ enablePreplanning: currentSettings.enablePreplanning,
138
+ autoBranchOnGoalStart: currentSettings.autoBranchOnGoalStart,
139
+ enablePostGoalRetrospective: currentSettings.enablePostGoalRetrospective,
140
+ autoScaleBudgetNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
141
+ enableTemplatesDrawer: currentSettings.enableTemplatesDrawer,
142
+ enableMilestoneDependencies: currentSettings.enableMilestoneDependencies,
143
+ });
144
+ }
145
145
 
146
- if (lastActiveAgent && lastActiveAgent.status === 'running' && typeof lastActiveAgent.cancel === 'function') {
147
- const lastSid = sessionIdOf(lastActiveAgent, null);
148
- if (!lastSid || lastSid === sid || sid === 'default') {
146
+ // 1. One-click auto-updater registration (Issue #99)
147
+ if (ctx?.webServer?.register) {
148
+ if (typeof ctx?.effect === 'function') {
149
+ ctx.effect(() => {
150
+ let unreg = null;
149
151
  try {
150
- lastActiveAgent.cancel({ kind: 'user' });
152
+ unreg = registerPluginUpdater(ctx, {
153
+ packageName: '@goodandready/dsh-goal',
154
+ endpoint: '/api/dsh-goal/update',
155
+ manifestUrl: new URL('../package.json', import.meta.url),
156
+ });
151
157
  } catch (err) {
152
- logger.warn('Failed to cancel lastActiveAgent:', err);
158
+ logger.warn?.('Failed to register plugin updater:', err.message);
153
159
  }
154
- }
160
+ return () => {
161
+ if (typeof unreg === 'function') unreg();
162
+ };
163
+ }, 'dsh-goal: plugin auto-updater');
164
+ } else {
165
+ registerPluginUpdater(ctx, {
166
+ packageName: '@goodandready/dsh-goal',
167
+ endpoint: '/api/dsh-goal/update',
168
+ manifestUrl: new URL('../package.json', import.meta.url),
169
+ });
155
170
  }
156
- };
157
-
158
- const resumeActiveAgent = (promptText, sessionId = 'default') => {
159
- const sid = sessionId || 'default';
160
- const snap = engine.getSnapshot(sid);
161
- const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
162
- const defaultResumePrompt = lang === 'zh'
163
- ? '▶️ 用户已恢复目标。请继续执行工作计划,并通过 goal_update_progress 更新进度。'
164
- : '▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.';
171
+ }
165
172
 
166
- const resumeMsg = createGoalUserMessage(promptText || defaultResumePrompt);
173
+ // 2. WebServer HTTP & SSE routes (registered after updater so it remains primary)
174
+ const { broadcastEvent } = registerRoutes(ctx, {
175
+ engine: fallbackEngine,
176
+ milestoneManager,
177
+ gitSnapshotManager,
178
+ tokenBudgetGuard,
179
+ addonStore,
180
+ goalsService: safeService(ctx, "goals"),
181
+ getConfig: () => currentSettings,
182
+ sseClients,
183
+ logger,
184
+ });
167
185
 
168
- let target = sessionAgents.get(sid);
169
- if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
170
- const lastSid = sessionIdOf(lastActiveAgent, null);
171
- if (!lastSid || lastSid === sid || sid === 'default') {
172
- target = lastActiveAgent;
186
+ // 3. Listen to core goal/changed events if ctx.on exists
187
+ if (typeof ctx?.on === 'function') {
188
+ ctx.on('goal/changed', async ({ agent, change }) => {
189
+ if (!change) return;
190
+ const op = change.operation;
191
+ const goal = change.goal;
192
+ const goalId = goal?.id || change.ref?.id;
193
+
194
+ if (op === 'create' || op === 'resume') {
195
+ tokenBudgetGuard.resetGoal(goalId);
196
+ broadcastEvent('goal_status', { op, goalId, goal });
173
197
  }
174
- }
175
198
 
176
- if (!target && agentsService && typeof agentsService.list === 'function') {
177
- const list = agentsService.list();
178
- if (list && list.length > 0) {
179
- for (let i = list.length - 1; i >= 0; i--) {
180
- const cand = list[i];
181
- const candSid = sessionIdOf(cand, null);
182
- if (candSid === sid) {
183
- target = cand;
184
- break;
199
+ if (op === 'complete' || op === 'clear') {
200
+ try {
201
+ const milestones = milestoneManager.getMilestones(goalId);
202
+ const snapshots = addonStore.getSnapshots(goalId);
203
+ const savedPath = saveGoalReportFile(goal || { id: goalId, objective: 'Completed Goal' }, {
204
+ milestones,
205
+ snapshots,
206
+ });
207
+ if (savedPath) {
208
+ logger.info?.(`[dsh-goal] Saved goal report to "${savedPath}"`);
185
209
  }
210
+ } catch (err) {
211
+ logger.warn?.('[dsh-goal] Failed to save goal report on completion:', err.message);
186
212
  }
187
- if (!target && sid === 'default') {
188
- target = list[list.length - 1];
189
- }
190
- }
191
- }
192
-
193
- if (target && typeof target.followup === 'function') {
194
- try {
195
- target.followup(resumeMsg);
196
- lastActiveAgent = target;
197
- sessionAgents.set(sid, target);
198
- return true;
199
- } catch (err) {
200
- logger.warn('Failed to resume agent:', err);
213
+ broadcastEvent('goal_status', { op, goalId, goal });
201
214
  }
202
- }
203
- return false;
204
- };
205
215
 
206
- const getConfig = () => {
207
- if (settingsScope) {
208
- const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
209
- let live = null;
210
- if (snap) {
211
- if (snap.status === 'ready' || snap.status === 'writable' || snap.status === undefined) {
212
- live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
213
- }
214
- } else if (typeof settingsScope.get === 'function') {
215
- live = settingsScope.get();
216
- }
217
- if (live && typeof live === 'object') {
218
- return {
219
- ...currentSettings,
220
- ...live,
221
- };
216
+ if (op === 'block') {
217
+ logger.info?.(`[dsh-goal] Goal "${goalId}" blocked: ${goal?.blockedReason?.message || 'Policy block'}`);
218
+ broadcastEvent('goal_status', { op, goalId, goal });
222
219
  }
223
- }
224
- return currentSettings;
225
- };
226
-
227
- const applySettings = () => {
228
- const live = getConfig();
229
- engine.updateConfig({
230
- defaultMaxIterations: live.maxIterations,
231
- autoDrive: live.autoDrive,
232
- enableSound: live.enableSound,
233
- showQuickLaunchButton: live.showQuickLaunchButton,
234
- consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
235
- maxTokenBudget: live.maxTokenBudget,
236
- budgetWarningThreshold: live.budgetWarningThreshold,
237
- autoCheckpointOnMilestone: live.autoCheckpointOnMilestone,
238
- enableLiveActivityFeed: live.enableLiveActivityFeed,
239
- enablePreplanning: live.enablePreplanning,
240
- autoBranchOnGoalStart: live.autoBranchOnGoalStart,
241
- enablePostGoalRetrospective: live.enablePostGoalRetrospective,
242
- autoScaleBudgetNearCompletion: live.autoScaleBudgetNearCompletion,
243
- enableTemplatesDrawer: live.enableTemplatesDrawer,
244
- enableMilestoneDependencies: live.enableMilestoneDependencies,
245
- soundScheme: live.soundScheme,
246
- enableVoiceAnnouncements: live.enableVoiceAnnouncements,
247
220
  });
248
- };
249
221
 
250
- // 1. Settings registration
251
- ctx.inject(['settings'], (sctx) => {
252
- try {
253
- const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
254
- if (scope) {
255
- settingsScope = scope;
256
- applySettings();
257
- if (typeof scope.subscribe === 'function') {
258
- sctx.effect(() => {
259
- const off = scope.subscribe(() => {
260
- applySettings();
222
+ // Inject milestone prompts and check budget during agent turns
223
+ ctx.on('agent/pre-step', async ({ agent, messages, step, signal }, next) => {
224
+ const goalsSvc = safeService(ctx, "goals");
225
+ if (goalsSvc && typeof goalsSvc.get === "function" && agent) {
226
+ try {
227
+ const currentGoal = goalsSvc.get(agent);
228
+ if (currentGoal && currentGoal.phase === 'active') {
229
+ const goalId = currentGoal.id;
230
+
231
+ // 1. Budget check
232
+ const milestones = milestoneManager.getMilestones(goalId);
233
+ const budgetCheck = tokenBudgetGuard.checkBudget({
234
+ agent,
235
+ goal: currentGoal,
236
+ tokenMeter: safeService(ctx, "tokenMeter"),
237
+ goalsService: goalsSvc,
238
+ milestones,
261
239
  });
262
- return () => {
263
- if (typeof off === 'function') off();
264
- settingsScope = null;
265
- };
266
- }, 'dsh-goal: settings subscription');
267
- }
268
- }
269
- } catch (err) {
270
- logger.warn('Settings register skipped:', err.message);
271
- }
272
- });
273
-
274
- // 2. Chat slash command /goal registration
275
- ctx.inject(['commands'], (cctx) => {
276
- try {
277
- if (typeof cctx.commands?.register !== 'function') return;
278
- const unregister = cctx.commands.register({
279
- name: 'goal',
280
- description: 'Manage Goal Mode: activate, milestones, pause, resume, and clear',
281
- input: { hint: '[<objective>|clear|pause|resume]' },
282
- handler: async (invocation) => {
283
- const sid = sessionIdOf(invocation, 'default');
284
- if (invocation?.agent) {
285
- lastActiveAgent = invocation.agent;
286
- sessionAgents.set(sid, invocation.agent);
287
- }
288
- const parsed = parseGoalInput(invocation?.rawInput);
289
- const liveConfig = getConfig();
290
- return executeGoalSlashCommand(engine, parsed, liveConfig, invocation?.agent, sid);
291
- },
292
- });
293
-
294
- if (typeof unregister === 'function') {
295
- cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
296
- }
297
- } catch (err) {
298
- logger.warn('Commands register skipped:', err.message);
299
- }
300
- });
301
-
302
- // 3. System prompt context injection
303
- ctx.inject(['systemPrompt'], (pctx) => {
304
- try {
305
- if (typeof pctx.systemPrompt?.section === 'function') {
306
- const globalSections = pctx.systemPrompt.layers?.global?.sections;
307
- let displacedPromptSection = null;
308
- if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
309
- displacedPromptSection = globalSections.data.get('tool:goal');
310
- globalSections.data.delete('tool:goal');
311
- }
312
240
 
313
- const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
314
- ? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 2400)
315
- : 2400;
316
-
317
- const unregisterSection = pctx.systemPrompt.section({
318
- name: 'tool:dsh-goal',
319
- order,
320
- text: (sessionCtx) => {
321
- const sid = sessionIdOf(sessionCtx, 'default');
322
- return engine.getStatePromptInjection(sid);
323
- },
324
- });
325
-
326
- if (typeof unregisterSection === 'function') {
327
- pctx.effect(() => () => {
328
- unregisterSection();
329
- if (displacedPromptSection && globalSections?.data instanceof Map) {
330
- globalSections.data.set('tool:goal', displacedPromptSection);
241
+ // 2. Active milestone prompt guidance
242
+ const milestonePrompt = milestoneManager.getActiveMilestonePrompt(goalId);
243
+ if (milestonePrompt && Array.isArray(messages)) {
244
+ let promptText = milestonePrompt;
245
+ if (budgetCheck.status === 'warning') {
246
+ promptText += `\n${budgetCheck.message}`;
247
+ } else if (budgetCheck.status === 'auto_scaled') {
248
+ promptText += `\n${budgetCheck.message}`;
249
+ }
250
+ messages.push({
251
+ role: 'system',
252
+ content: promptText,
253
+ });
331
254
  }
332
- }, 'dsh-goal: system prompt section');
255
+ }
256
+ } catch (err) {
257
+ logger.warn?.('[dsh-goal] agent/pre-step error:', err.message);
333
258
  }
334
259
  }
335
- } catch (err) {
336
- logger.warn('SystemPrompt section register skipped:', err.message);
337
- }
338
- });
260
+ return next ? next() : undefined;
261
+ });
339
262
 
340
- // 4. Tools registration
341
- registerTools(ctx, { engine, resumeActiveAgent, logger });
263
+ // Compatibility events for turn/end and approval/asked
264
+ ctx.on('turn/end', (event) => {
265
+ // noop or session isolation
266
+ });
342
267
 
343
- // 5. One-click auto-updater registration with Cordis effect cleanup (Issue #99)
344
- if (ctx.webServer?.register) {
345
- ctx.effect(() => {
346
- let unreg = null;
347
- try {
348
- unreg = registerPluginUpdater(ctx, {
349
- packageName: '@goodandready/dsh-goal',
350
- endpoint: '/api/dsh-goal/update',
351
- manifestUrl: new URL('../package.json', import.meta.url),
352
- });
353
- } catch (err) {
354
- logger.warn('Failed to register plugin updater:', err.message);
268
+ ctx.on('approval/asked', (event) => {
269
+ if (!fallbackEngine) return;
270
+ const sid = sessionIdOf(event, 'default');
271
+ const snap = fallbackEngine.getSnapshot(sid);
272
+ if (snap && snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
273
+ fallbackEngine.pause('Waiting for operator confirmation/approval', sid);
355
274
  }
356
- return () => {
357
- if (typeof unreg === 'function') unreg();
358
- };
359
- }, 'dsh-goal: plugin auto-updater');
275
+ });
360
276
  }
361
277
 
362
- // 6. WebServer HTTP & SSE routes
363
- registerRoutes(ctx, {
364
- engine,
365
- getConfig,
366
- stopRunningAgents,
367
- resumeActiveAgent,
368
- sessionAgents,
369
- sseClients,
370
- });
371
-
372
- // 7. Turn coordinator & tool-failure breaker
373
- ctx.effect(() => {
374
- const onTurnEnd = (turn) => {
375
- const liveConfig = getConfig();
376
- if (!liveConfig.autoDrive) return;
377
-
378
- const sid = sessionIdOf(turn, 'default');
379
- const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
380
- if (usage) {
381
- engine.addTokenUsage(usage, sid);
382
- }
383
-
384
- const snap = engine.getSnapshot(sid);
385
- if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
386
- const steps = turn?.steps || turn?.turn?.steps || [];
387
- const hasStepError = Array.isArray(steps) && steps.some((s) => {
388
- return s?.status === 'error' || s?.error || s?.toolResult?.isError;
389
- });
390
- const hasTurnError = Boolean(turn?.error || turn?.reason?.kind === 'error');
391
- const isToolFailure = hasStepError || hasTurnError;
278
+ // Settings synchronization
279
+ const applySettings = (patch) => {
280
+ if (!patch || typeof patch !== 'object') return;
281
+ currentSettings = { ...currentSettings, ...patch };
282
+ tokenBudgetGuard.updateConfig({
283
+ maxTokenBudget: currentSettings.maxTokenBudget,
284
+ warningThreshold: currentSettings.budgetWarningThreshold,
285
+ autoScaleNearCompletion: currentSettings.autoScaleBudgetNearCompletion,
286
+ });
287
+ milestoneManager.enforceDependencies = Boolean(currentSettings.enableMilestoneDependencies);
288
+ if (typeof patch.storagePath === 'string' && patch.storagePath.trim()) {
289
+ addonStore.setStoragePath(patch.storagePath.trim());
290
+ }
291
+ };
392
292
 
393
- if (isToolFailure) {
394
- const failCount = engine.incrementToolFailureCount(sid);
395
- const limit = liveConfig.consecutiveToolFailureLimit;
396
- if (limit > 0 && failCount >= limit) {
397
- const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
398
- const pauseReason = lang === 'zh'
399
- ? `工具重复出错(连续 ${failCount} 次)— 目标已暂停以防止死循环`
400
- : `Repeated tool failure (${failCount} consecutive) — goal paused to prevent token burn`;
401
- engine.pause(pauseReason, sid);
402
- return;
293
+ if (typeof ctx?.inject === 'function') {
294
+ ctx.inject(['settings'], (sctx) => {
295
+ try {
296
+ const scope = sctx.settings?.register?.(NS, Config, { base: currentSettings });
297
+ if (scope) {
298
+ settingsScope = scope;
299
+ if (typeof scope.get === 'function') {
300
+ applySettings(scope.get());
403
301
  }
404
- } else {
405
- engine.resetToolFailureCount(sid);
406
- }
407
-
408
- if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
409
- return;
410
- }
411
-
412
- const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
413
- const stallCount = engine.incrementStallCount(sid);
414
- if (stallCount >= 2) {
415
- const stallReason = lang === 'zh'
416
- ? '智能体在过去 2 次迭代中未推进工作计划 — 需要操作员关注'
417
- : 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
418
- engine.pause(stallReason, sid);
419
- return;
420
- }
421
-
422
- const canContinue = engine.incrementIteration(sid);
423
- if (!canContinue) return;
424
-
425
- const triggerNextTurn = () => {
426
- const currentSnap = engine.getSnapshot(sid);
427
- if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
428
- const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
429
- let promptText = currentLang === 'zh'
430
- ? '请按照工作计划继续自主执行目标。每完成一步请通过 goal_update_progress 记录,所有任务完成后调用 goal_finish。'
431
- : 'Continue autonomous goal execution according to the work plan. Mark each completed step via goal_update_progress, and when all tasks are complete, call goal_finish.';
432
-
433
- if (currentSnap.pendingNudge) {
434
- const userNudge = engine.consumePendingNudge(sid);
435
- if (userNudge) {
436
- promptText = (currentLang === 'zh'
437
- ? `🚨 用户紧急说明/指导方向:\n"${userNudge}"\n请根据此反馈调整执行。\n\n`
438
- : `🚨 URGENT USER CLARIFICATION / STEERING:\n"${userNudge}"\nAdjust execution adhering to this feedback.\n\n`) + promptText;
439
- }
440
- }
441
-
442
- const promptMsg = createGoalUserMessage(promptText);
443
- let target = sessionAgents.get(sid);
444
- if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
445
- const lastSid = sessionIdOf(lastActiveAgent, null);
446
- if (!lastSid || lastSid === sid || sid === 'default') {
447
- target = lastActiveAgent;
448
- }
449
- }
450
-
451
- if (!target && agentsService && typeof agentsService.list === 'function') {
452
- const list = agentsService.list();
453
- if (list && list.length > 0) {
454
- for (let i = list.length - 1; i >= 0; i--) {
455
- const cand = list[i];
456
- if (sessionIdOf(cand, null) === sid) {
457
- target = cand;
458
- break;
459
- }
460
- }
461
- if (!target && sid === 'default') target = list[list.length - 1];
462
- }
463
- }
464
-
465
- if (target && typeof target.followup === 'function' && target.status !== 'stopped') {
466
- try {
467
- target.followup(promptMsg);
468
- sessionAgents.set(sid, target);
469
- } catch (err) {
470
- logger.warn('Failed auto-drive followup:', err);
471
- }
472
- }
302
+ if (typeof scope.subscribe === 'function') {
303
+ sctx.effect?.(() => {
304
+ const off = scope.subscribe(() => {
305
+ if (typeof scope.get === 'function') applySettings(scope.get());
306
+ });
307
+ return () => {
308
+ if (typeof off === 'function') off();
309
+ settingsScope = null;
310
+ };
311
+ }, 'dsh-goal: settings subscription');
473
312
  }
474
- };
475
-
476
- if (typeof setImmediate === 'function') {
477
- setImmediate(triggerNextTurn);
478
- } else {
479
- setTimeout(triggerNextTurn, 0);
480
313
  }
314
+ } catch (err) {
315
+ logger.warn?.('Settings register skipped:', err.message);
481
316
  }
482
- };
483
-
484
- const onApprovalAsked = (event) => {
485
- const sid = sessionIdOf(event, 'default');
486
- const snap = engine.getSnapshot(sid);
487
- if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
488
- const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
489
- const pauseReason = lang === 'zh'
490
- ? '等待操作员确认/批准操作'
491
- : 'Waiting for operator confirmation/approval';
492
- engine.pause(pauseReason, sid);
493
- }
494
- };
317
+ });
318
+ }
495
319
 
496
- ctx.on?.('turn/end', onTurnEnd);
497
- ctx.on?.('approval/asked', onApprovalAsked);
320
+ if (typeof ctx?.effect === 'function') {
321
+ ctx.effect(() => () => {
322
+ try {
323
+ addonStore.flushSync();
324
+ } catch (e) { /* ignore */ }
325
+ }, 'dsh-goal: teardown');
326
+ }
498
327
 
499
- return () => {
500
- ctx.off?.('turn/end', onTurnEnd);
501
- ctx.off?.('approval/asked', onApprovalAsked);
502
- };
503
- }, 'dsh-goal: Session & Turn Coordinator');
328
+ return {
329
+ addonStore,
330
+ milestoneManager,
331
+ tokenBudgetGuard,
332
+ gitSnapshotManager,
333
+ engine: fallbackEngine,
334
+ };
504
335
  }