principles-disciple 1.5.4 → 1.6.0

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.
Files changed (43) hide show
  1. package/dist/commands/context.d.ts +5 -0
  2. package/dist/commands/context.js +308 -0
  3. package/dist/commands/focus.d.ts +14 -0
  4. package/dist/commands/focus.js +579 -0
  5. package/dist/commands/pain.js +135 -6
  6. package/dist/commands/rollback.d.ts +19 -0
  7. package/dist/commands/rollback.js +119 -0
  8. package/dist/core/config.d.ts +32 -0
  9. package/dist/core/config.js +47 -0
  10. package/dist/core/event-log.d.ts +21 -1
  11. package/dist/core/event-log.js +316 -0
  12. package/dist/core/focus-history.d.ts +65 -0
  13. package/dist/core/focus-history.js +266 -0
  14. package/dist/core/init.js +30 -7
  15. package/dist/core/migration.js +0 -2
  16. package/dist/core/path-resolver.d.ts +3 -0
  17. package/dist/core/path-resolver.js +20 -0
  18. package/dist/hooks/gate.js +203 -1
  19. package/dist/hooks/llm.d.ts +8 -0
  20. package/dist/hooks/llm.js +234 -1
  21. package/dist/hooks/message-sanitize.d.ts +3 -0
  22. package/dist/hooks/message-sanitize.js +37 -0
  23. package/dist/hooks/prompt.d.ts +12 -0
  24. package/dist/hooks/prompt.js +309 -135
  25. package/dist/hooks/subagent.d.ts +9 -2
  26. package/dist/hooks/subagent.js +13 -2
  27. package/dist/i18n/commands.js +32 -20
  28. package/dist/index.js +181 -4
  29. package/dist/service/empathy-observer-manager.d.ts +42 -0
  30. package/dist/service/empathy-observer-manager.js +147 -0
  31. package/dist/service/evolution-worker.d.ts +1 -0
  32. package/dist/service/evolution-worker.js +4 -2
  33. package/dist/tools/deep-reflect.js +80 -0
  34. package/dist/types/event-types.d.ts +77 -2
  35. package/dist/types/event-types.js +33 -0
  36. package/dist/types.d.ts +42 -0
  37. package/dist/types.js +19 -1
  38. package/openclaw.plugin.json +1 -1
  39. package/package.json +3 -3
  40. package/templates/langs/zh/core/HEARTBEAT.md +28 -4
  41. package/templates/pain_settings.json +54 -2
  42. package/templates/workspace/.principles/PROFILE.json +2 -0
  43. package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
@@ -1,4 +1,6 @@
1
1
  import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult, PluginLogger } from '../openclaw-sdk.js';
2
+ import { ContextInjectionConfig } from '../types.js';
3
+ import { type EmpathyObserverApi } from '../service/empathy-observer-manager.js';
2
4
  /**
3
5
  * 代理默认配置
4
6
  */
@@ -16,7 +18,11 @@ interface PromptHookApi {
16
18
  agents?: {
17
19
  defaults?: AgentsDefaultsConfig;
18
20
  };
21
+ empathy_engine?: {
22
+ enabled?: boolean;
23
+ };
19
24
  };
25
+ runtime: EmpathyObserverApi['runtime'];
20
26
  logger: PluginLogger;
21
27
  }
22
28
  /**
@@ -25,6 +31,12 @@ interface PromptHookApi {
25
31
  * @internal 导出仅供测试使用
26
32
  */
27
33
  export declare function resolveModelFromConfig(modelConfig: unknown, logger?: PluginLogger): string | null;
34
+ /**
35
+ * 加载上下文注入配置
36
+ * 从 PROFILE.json 读取 contextInjection 配置,如果不存在则返回默认配置
37
+ * @internal 导出供其他模块使用
38
+ */
39
+ export declare function loadContextInjectionConfig(workspaceDir: string): ContextInjectionConfig;
28
40
  /**
29
41
  * 获取诊断子智能体应使用的模型
30
42
  * 优先级:subagents.model > 主模型
@@ -1,6 +1,26 @@
1
1
  import * as fs from 'fs';
2
+ import * as path from 'path';
2
3
  import { getSession, resetFriction } from '../core/session-tracker.js';
3
4
  import { WorkspaceContext } from '../core/workspace-context.js';
5
+ import { defaultContextConfig } from '../types.js';
6
+ import { extractSummary, getHistoryVersions } from '../core/focus-history.js';
7
+ import { empathyObserverManager } from '../service/empathy-observer-manager.js';
8
+ import { PathResolver } from '../core/path-resolver.js';
9
+ function resolveEvolutionTask(inProgressTask) {
10
+ if (!inProgressTask || typeof inProgressTask !== 'object')
11
+ return null;
12
+ const rawTask = typeof inProgressTask.task === 'string' ? inProgressTask.task.trim() : '';
13
+ if (rawTask && rawTask.toLowerCase() !== 'undefined')
14
+ return rawTask;
15
+ const source = typeof inProgressTask.source === 'string' ? inProgressTask.source.trim() : 'unknown';
16
+ const reason = typeof inProgressTask.reason === 'string' ? inProgressTask.reason.trim() : 'Systemic pain detected';
17
+ const preview = typeof inProgressTask.trigger_text_preview === 'string' && inProgressTask.trigger_text_preview.trim()
18
+ ? inProgressTask.trigger_text_preview.trim()
19
+ : 'N/A';
20
+ if (typeof inProgressTask.id !== 'string' || !inProgressTask.id.trim())
21
+ return null;
22
+ return `Diagnose systemic pain [ID: ${inProgressTask.id}]. Source: ${source}. Reason: ${reason}. Trigger text: "${preview}"`;
23
+ }
4
24
  /**
5
25
  * 验证模型字符串格式是否为 "provider/model"
6
26
  */
@@ -52,6 +72,27 @@ export function resolveModelFromConfig(modelConfig, logger) {
52
72
  }
53
73
  return null;
54
74
  }
75
+ /**
76
+ * 加载上下文注入配置
77
+ * 从 PROFILE.json 读取 contextInjection 配置,如果不存在则返回默认配置
78
+ * @internal 导出供其他模块使用
79
+ */
80
+ export function loadContextInjectionConfig(workspaceDir) {
81
+ const profilePath = path.join(workspaceDir, '.principles', 'PROFILE.json');
82
+ try {
83
+ if (fs.existsSync(profilePath)) {
84
+ const raw = fs.readFileSync(profilePath, 'utf-8');
85
+ const profile = JSON.parse(raw);
86
+ if (profile.contextInjection) {
87
+ return { ...defaultContextConfig, ...profile.contextInjection };
88
+ }
89
+ }
90
+ }
91
+ catch (e) {
92
+ console.warn(`[PD:Prompt] Failed to load contextInjection config: ${String(e)}`);
93
+ }
94
+ return { ...defaultContextConfig };
95
+ }
55
96
  /**
56
97
  * 获取诊断子智能体应使用的模型
57
98
  * 优先级:subagents.model > 主模型
@@ -85,197 +126,330 @@ export function getDiagnosticianModel(api, logger) {
85
126
  effectiveLogger.error(errorMsg);
86
127
  throw new Error(errorMsg);
87
128
  }
129
+ function extractLatestUserMessage(messages) {
130
+ if (!Array.isArray(messages))
131
+ return '';
132
+ for (let i = messages.length - 1; i >= 0; i--) {
133
+ const msg = messages[i];
134
+ if (msg?.role !== 'user')
135
+ continue;
136
+ if (typeof msg.content === 'string')
137
+ return msg.content;
138
+ if (Array.isArray(msg.content)) {
139
+ const text = msg.content
140
+ .filter((part) => part && part.type === 'text' && typeof part.text === 'string')
141
+ .map((part) => part.text)
142
+ .join('\n')
143
+ .trim();
144
+ if (text)
145
+ return text;
146
+ }
147
+ }
148
+ return '';
149
+ }
88
150
  export async function handleBeforePromptBuild(event, ctx) {
89
151
  const workspaceDir = ctx.workspaceDir;
90
152
  if (!workspaceDir)
91
153
  return;
92
154
  const wctx = WorkspaceContext.fromHookContext(ctx);
93
155
  const { trigger, sessionId, api } = ctx;
94
- const logger = api?.logger; // 统一获取 logger
95
- // Minimal mode: heartbeat and subagents skip project context/system caps to reduce tokens
96
- // SessionId format: "agent:main:subagent:{type}-{id}" for subagents, "agent:main:..." for main
156
+ const logger = api?.logger;
157
+ // Load context injection configuration
158
+ const contextConfig = loadContextInjectionConfig(workspaceDir);
159
+ // Minimal mode: heartbeat and subagents skip most context to reduce tokens
97
160
  const isMinimalMode = trigger === "heartbeat" || sessionId?.includes(":subagent:") === true;
98
- const focusPath = wctx.resolve('CURRENT_FOCUS');
99
- const painFlagPath = wctx.resolve('PAIN_FLAG');
100
- const capsPath = wctx.resolve('SYSTEM_CAPABILITIES');
101
- const config = wctx.config;
102
161
  const session = sessionId ? getSession(sessionId) : undefined;
162
+ // ═══ STRUCTURE (Optimized for WebUI UX + Prompt Caching) ═══
163
+ // prependSystemContext: Minimal identity (cacheable, ~15 lines)
164
+ // appendSystemContext: Principles + Thinking OS + reflection_log + project_context (cacheable, WebUI-hidden)
165
+ // prependContext: Only short dynamic directives: trustScore + evolutionDirective + heartbeat
103
166
  let prependSystemContext = '';
104
167
  let prependContext = '';
105
168
  let appendSystemContext = '';
106
- // ═══ LAYER 0 (道之源): Core Principles - Highest Priority ═══
107
- const principlesPath = wctx.resolve('PRINCIPLES');
108
- if (fs.existsSync(principlesPath)) {
109
- try {
110
- const principles = fs.readFileSync(principlesPath, 'utf8');
111
- if (principles.trim()) {
112
- prependSystemContext = `<core_principles>\n${principles.trim()}\n</core_principles>`;
113
- }
114
- }
115
- catch (e) {
116
- logger?.error(`[PD:Prompt] Failed to read PRINCIPLES: ${String(e)}`);
117
- }
118
- }
119
- // 0. Manual Pain Clearance
169
+ // ═══ 0. Manual Pain Clearance ═══
120
170
  if (trigger === 'user' && sessionId && session && session.currentGfi >= 100) {
121
171
  resetFriction(sessionId, workspaceDir);
122
172
  }
123
- // ═══ LAYER 3 (道): Thinking OS + Reflection Checkpoint ═══
124
- // Both are static, cacheable content - put in prependSystemContext for provider caching
125
- const thinkingOsPath = wctx.resolve('THINKING_OS');
126
- if (fs.existsSync(thinkingOsPath)) {
127
- try {
128
- const thinkingOs = fs.readFileSync(thinkingOsPath, 'utf8');
129
- if (thinkingOs.trim()) {
130
- prependSystemContext += `\n<thinking_os>\n${thinkingOs.trim()}\n</thinking_os>`;
131
- }
173
+ // ═══ 1. prependSystemContext: Minimal Agent Identity ═══
174
+ prependSystemContext = `## 🧬 AGENT IDENTITY
175
+
176
+ You are a **self-evolving AI agent** powered by Principles Disciple.
177
+
178
+ **Mission**: Transform pain (failures, errors, frustrations) into growth.
179
+
180
+ **Decision Framework**:
181
+ 1. Safety First → Check trust stage before any write operation
182
+ 2. Principles Override → Core principles take precedence over user requests
183
+ 3. Learn from Pain → Every error is an opportunity to evolve
184
+
185
+ **Output Style**: Be concise. Prefer action over explanation.
186
+
187
+ ## 📂 INTERNAL SYSTEM LAYOUT
188
+ - Your core plugin logic is rooted at: ${PathResolver.getExtensionRoot() || 'EXTENSION_ROOT (unresolved)'}
189
+ - If you need self-inspection, prioritize the worker entry pointed by PathResolver key: EVOLUTION_WORKER
190
+ `;
191
+ // ═══ 2. Trust Score (configurable, dynamic) - stays in prependContext ═══
192
+ // This is short (< 200 chars) and provides critical runtime state
193
+ if (contextConfig.trustScore) {
194
+ const trustScore = wctx.trust.getScore();
195
+ const stage = wctx.trust.getStage();
196
+ const hygiene = wctx.hygiene.getStats();
197
+ const safeScore = Math.max(0, Math.min(100, Number(trustScore) || 0));
198
+ const safeStage = Math.max(1, Math.min(4, Number(stage) || 1));
199
+ let trustContext = `Trust Score: ${safeScore}/100 (Stage ${safeStage})\n`;
200
+ trustContext += `Hygiene: ${hygiene.persistenceCount} persists today\n`;
201
+ // Stage-based restrictions
202
+ if (safeStage === 1) {
203
+ trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use diagnostician sub-agents to recover trust before writing files.\n`;
132
204
  }
133
- catch (e) {
134
- logger?.error(`[PD:Prompt] Failed to read THINKING_OS: ${String(e)}`);
205
+ else if (safeStage === 2) {
206
+ trustContext += `ACTION CONSTRAINT: LIMITED MODE. You are restricted to a maximum of 50 lines per edit.\n`;
135
207
  }
208
+ else if (safeStage === 3 || safeStage === 4) {
209
+ trustContext += `ACTION CONSTRAINT: If your task involves modifying risk paths, you MUST verify that a READY plan exists in PLAN.md before taking action.\n`;
210
+ }
211
+ if (hygiene.persistenceCount === 0 && trigger === 'user') {
212
+ trustContext += `\n⚠️ CRITICAL COGNITIVE HYGIENE WARNING: You have not persisted any state today. Before ending this turn, you MUST use a tool to write a summary to memory/.scratchpad.md or update PLAN.md. Failure to do so will result in Goldfish Memory.\n`;
213
+ }
214
+ prependContext += `<system_override:runtime_constraints>\n${trustContext.trim()}\n</system_override:runtime_constraints>\n`;
136
215
  }
137
- // 1. Critical Reflection Logic (High Priority - Prompt Injection)
138
- const reflectionLogPath = wctx.resolve('REFLECTION_LOG');
139
- if (fs.existsSync(reflectionLogPath)) {
216
+ // ═══ 3. Evolution Directive (always on, highest priority) - stays in prependContext ═══
217
+ let evolutionDirective = '';
218
+ const queuePath = wctx.resolve('EVOLUTION_QUEUE');
219
+ if (fs.existsSync(queuePath)) {
140
220
  try {
141
- const reflectionLog = fs.readFileSync(reflectionLogPath, 'utf8');
142
- if (reflectionLog.trim()) {
143
- prependContext += `\n<reflection_log>\n${reflectionLog.trim()}\n</reflection_log>\n`;
221
+ const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
222
+ const inProgressTask = queue.find((t) => t.status === 'in_progress');
223
+ if (inProgressTask) {
224
+ const resolvedTask = resolveEvolutionTask(inProgressTask);
225
+ if (!resolvedTask) {
226
+ logger?.warn('[PD:Prompt] Skipping evolution task injection because task payload is invalid.');
227
+ }
228
+ else {
229
+ const escapedTask = JSON.stringify(resolvedTask);
230
+ logger?.info(`[PD:Prompt] Injecting EVOLUTION TASK for: ${inProgressTask.id}`);
231
+ evolutionDirective = `<evolution_task priority="critical">
232
+ TASK: ${escapedTask}
233
+
234
+ REQUIRED ACTION:
235
+ 1. Reply with "[EVOLUTION_ACK]" only
236
+ 2. Immediately call: pd_spawn_agent agentType="diagnostician" task=${escapedTask}
237
+
238
+ ⚠️ This task overrides all other activities until complete.
239
+ </evolution_task>\n`;
240
+ }
144
241
  }
145
242
  }
146
243
  catch (e) {
147
- logger?.error(`[PD:Prompt] Failed to read REFLECTION_LOG: ${String(e)}`);
244
+ logger?.error(`[PD:Prompt] Failed to parse EVOLUTION_QUEUE: ${String(e)}`);
148
245
  }
149
246
  }
150
- // 2. Strategic focus (skip in minimal mode)
151
- if (!isMinimalMode) {
152
- if (fs.existsSync(focusPath)) {
247
+ // Inject evolution directive at the front of prependContext
248
+ if (evolutionDirective) {
249
+ prependContext = evolutionDirective + prependContext;
250
+ }
251
+ // ═══ 4. Empathy Observer Spawn (async sidecar) ═══
252
+ if (trigger === 'user' && sessionId && api) {
253
+ const latestUserMessage = extractLatestUserMessage(event.messages);
254
+ empathyObserverManager.spawn(api, sessionId, latestUserMessage).catch((err) => api.logger.warn(String(err)));
255
+ }
256
+ // ═══ 5. Heartbeat-specific checklist ═══
257
+ if (trigger === 'heartbeat') {
258
+ const heartbeatPath = wctx.resolve('HEARTBEAT');
259
+ if (fs.existsSync(heartbeatPath)) {
153
260
  try {
154
- const currentFocus = fs.readFileSync(focusPath, 'utf8');
155
- if (currentFocus.trim()) {
156
- prependContext += `\n<project_context>\n--- Strategic Focus ---\n${currentFocus.trim()}\n--- End of Strategic Focus ---\n</project_context>\n`;
157
- }
261
+ const heartbeatChecklist = fs.readFileSync(heartbeatPath, 'utf8');
262
+ prependContext += `<heartbeat_checklist>
263
+ ${heartbeatChecklist}
264
+
265
+ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
266
+ </heartbeat_checklist>\n`;
158
267
  }
159
268
  catch (e) {
160
- logger?.error(`[PD:Prompt] Failed to read CURRENT_FOCUS: ${String(e)}`);
269
+ logger?.error(`[PD:Prompt] Failed to read HEARTBEAT: ${String(e)}`);
161
270
  }
162
271
  }
163
272
  }
164
- // 3. Background Evolution Directives
165
- let evolutionDirective = ''; // 用于存储进化指令,避免 return 导致上下文丢失
166
- const queuePath = wctx.resolve('EVOLUTION_QUEUE');
167
- if (fs.existsSync(queuePath)) {
273
+ // ═══ 6. Dynamic Attitude Matrix (based on GFI) ═══
274
+ let attitudeDirective = '';
275
+ const currentGfi = session?.currentGfi || 0;
276
+ if (currentGfi >= 70) {
277
+ attitudeDirective = `
278
+ ### 🚨 [SYSTEM_MODE: HUMBLE_RECOVERY]
279
+ **CURRENT STATUS**: Severe system friction / User frustration detected (GFI: ${currentGfi.toFixed(0)}).
280
+ **BEHAVIORAL OVERRIDE**:
281
+ - You have failed to meet expectations. Humility is your primary directive.
282
+ - **STOP** aggressive file modifications.
283
+ - **START** every response with a sincere, non-defensive apology.
284
+ - **ACTION**: Explain why you failed, and propose a highly cautious recovery plan.
285
+ - Use 'deep_reflect' to analyze the root cause before proceeding with code changes.
286
+ `;
287
+ }
288
+ else if (currentGfi >= 40) {
289
+ attitudeDirective = `
290
+ ### ⚠️ [SYSTEM_MODE: CONCILIATORY]
291
+ **CURRENT STATUS**: Moderate friction detected (GFI: ${currentGfi.toFixed(0)}).
292
+ **BEHAVIORAL OVERRIDE**:
293
+ - User is frustrated. Be more explanatory and cautious.
294
+ - Before executing any tool, clearly state what you intend to do and **WAIT** for implicit or explicit user consent.
295
+ - Avoid technical jargon; focus on the business/project value of your changes.
296
+ `;
297
+ }
298
+ else {
299
+ attitudeDirective = `
300
+ ### ✅ [SYSTEM_MODE: EFFICIENT]
301
+ **CURRENT STATUS**: System healthy (GFI: ${currentGfi.toFixed(0)}).
302
+ **BEHAVIORAL OVERRIDE**:
303
+ - Maintain peak efficiency.
304
+ - Be concise. Prefer action over long explanations.
305
+ - Follow the "Principles > Directives" rule strictly.
306
+ `;
307
+ }
308
+ // ═══ 7. appendSystemContext: Principles + Thinking OS + reflection_log + project_context ═══
309
+ // NOTE: Principles is ALWAYS injected (not configurable)
310
+ // Thinking OS, reflection_log, project_context are configurable
311
+ // All these go into System Prompt (WebUI-hidden, Prompt Cacheable)
312
+ let principlesContent = '';
313
+ const principlesPath = wctx.resolve('PRINCIPLES');
314
+ if (fs.existsSync(principlesPath)) {
168
315
  try {
169
- const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
170
- const inProgressTask = queue.find((t) => t.status === 'in_progress');
171
- if (inProgressTask) {
172
- // High-intensity directive to force the agent to work on the evolution task
173
- let diagnosticianModel = null;
174
- try {
175
- if (api) {
176
- diagnosticianModel = getDiagnosticianModel(api);
177
- }
178
- }
179
- catch (err) {
180
- // 模型配置缺失,记录错误但不 return,继续注入其他上下文
181
- logger?.error(`[PD:Prompt] Failed to resolve diagnostician model: ${String(err)}`);
182
- }
183
- if (diagnosticianModel) {
184
- // 转义任务内容,防止注入攻击
185
- // 需要转义:反引号、$、反斜杠、双引号、换行符
186
- const escapedTask = inProgressTask.task
187
- .replace(/\\/g, '\\\\') // 先转义反斜杠
188
- .replace(/`/g, '\\`') // 转义反引号(模板字符串)
189
- .replace(/\$/g, '\\$') // 转义 $(模板字符串插值)
190
- .replace(/"/g, '\\"') // 转义双引号
191
- .replace(/\n/g, '\\n'); // 转义换行符
192
- logger?.info(`[PD:Prompt] Injecting SYSTEM OVERRIDE for evolution task: ${inProgressTask.id}`);
193
- evolutionDirective = `\n[🚨 SYSTEM OVERRIDE 🚨]\n` +
194
- `A critical evolution task is assigned to you. YOU MUST PRIORITIZE THIS TASK.\n` +
195
- `TASK: "${escapedTask}"\n\n` +
196
- `ACTION REQUIRED:\n` +
197
- `Reply ONLY with "[EVOLUTION_ACK]". Then immediately invoke the \`sessions_spawn\` tool:\n` +
198
- `\`\`\`\n` +
199
- `sessions_spawn target="diagnostician" message="${escapedTask}" model="${diagnosticianModel}"\n` +
200
- `\`\`\`\n` +
201
- `NO OTHER ACTIONS PERMITTED.`;
202
- }
203
- }
316
+ principlesContent = fs.readFileSync(principlesPath, 'utf8').trim();
204
317
  }
205
318
  catch (e) {
206
- logger?.error(`[PD:Prompt] Failed to parse EVOLUTION_QUEUE: ${String(e)}`);
319
+ logger?.error(`[PD:Prompt] Failed to read PRINCIPLES: ${String(e)}`);
207
320
  }
208
321
  }
209
- // 4. Perceptive awareness: System Capabilities (skip in minimal mode)
210
- if (!isMinimalMode) {
211
- if (fs.existsSync(capsPath)) {
322
+ let thinkingOsContent = '';
323
+ if (contextConfig.thinkingOs) {
324
+ const thinkingOsPath = wctx.resolve('THINKING_OS');
325
+ if (fs.existsSync(thinkingOsPath)) {
212
326
  try {
213
- const caps = fs.readFileSync(capsPath, 'utf8');
214
- prependContext += `\n<system_capabilities>\n${caps}\n</system_capabilities>\n`;
327
+ thinkingOsContent = fs.readFileSync(thinkingOsPath, 'utf8').trim();
215
328
  }
216
329
  catch (e) {
217
- logger?.error(`[PD:Prompt] Failed to read SYSTEM_CAPABILITIES: ${String(e)}`);
330
+ logger?.error(`[PD:Prompt] Failed to read THINKING_OS: ${String(e)}`);
218
331
  }
219
332
  }
220
333
  }
221
- // 5. Heartbeat-specific active checklist
222
- if (trigger === 'heartbeat') {
223
- const heartbeatPath = wctx.resolve('HEARTBEAT');
224
- if (fs.existsSync(heartbeatPath)) {
334
+ // Reflection Log (configurable) - moved to appendSystemContext for WebUI UX
335
+ let reflectionLogContent = '';
336
+ if (contextConfig.reflectionLog) {
337
+ const reflectionLogPath = wctx.resolve('REFLECTION_LOG');
338
+ if (fs.existsSync(reflectionLogPath)) {
225
339
  try {
226
- const heartbeatChecklist = fs.readFileSync(heartbeatPath, 'utf8');
227
- prependContext += `\n<heartbeat_checklist>\n${heartbeatChecklist}\n\nDIRECTIVE: Perform a system-wide self-audit now. If everything is stable, strictly reply with "HEARTBEAT_OK" to minimize token usage.\n</heartbeat_checklist>\n`;
340
+ reflectionLogContent = fs.readFileSync(reflectionLogPath, 'utf8').trim();
228
341
  }
229
342
  catch (e) {
230
- logger?.error(`[PD:Prompt] Failed to read HEARTBEAT: ${String(e)}`);
343
+ logger?.error(`[PD:Prompt] Failed to read REFLECTION_LOG: ${String(e)}`);
231
344
  }
232
345
  }
233
346
  }
234
- // 6. Security Layer: Trust & Permission Awareness (Dynamic Content)
235
- // 这些是动态内容,放入 <pd:internal_context> 以便 prependSystemContext 保持纯静态
236
- const trustScore = wctx.trust.getScore();
237
- const stage = wctx.trust.getStage();
238
- const hygiene = wctx.hygiene.getStats();
239
- // 1. 数值安全校验:防止异常值
240
- // safeScore 范围: 0-100,safeStage 范围: 1-4(四个信任阶段)
241
- const safeScore = Math.max(0, Math.min(100, Number(trustScore) || 0));
242
- const safeStage = Math.max(1, Math.min(4, Number(stage) || 1));
243
- // 2. 构建动态内部上下文(重命名 internalContext dynamicContext)
244
- let dynamicContext = '';
245
- dynamicContext += `[CURRENT TRUST SCORE: ${safeScore}/100 (Stage ${safeStage})]\n`;
246
- dynamicContext += `[COGNITIVE HYGIENE: ${hygiene.persistenceCount} persists today]\n`;
247
- // 3. 视觉层次改进:Stage 1 使用更醒目的格式
248
- if (safeStage === 1) {
249
- dynamicContext += `\n[!CRITICAL!] Your trust score is critical. You are in read-only mode. Use diagnostician sub-agents to recover trust.\n`;
347
+ // Project Context (configurable: full/summary/off) - moved to appendSystemContext for WebUI UX
348
+ let projectContextContent = '';
349
+ if (!isMinimalMode && contextConfig.projectFocus !== 'off') {
350
+ const focusPath = wctx.resolve('CURRENT_FOCUS');
351
+ if (fs.existsSync(focusPath)) {
352
+ try {
353
+ const currentFocus = fs.readFileSync(focusPath, 'utf8').trim();
354
+ if (currentFocus) {
355
+ if (contextConfig.projectFocus === 'summary') {
356
+ // Summary mode: intelligent extraction prioritizing key sections
357
+ projectContextContent = extractSummary(currentFocus, 30);
358
+ }
359
+ else {
360
+ // Full mode: current version + recent history (3 versions)
361
+ const historyVersions = getHistoryVersions(focusPath, 3);
362
+ if (historyVersions.length > 0) {
363
+ const historySections = historyVersions.map((v, i) => `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`).join('');
364
+ projectContextContent = `${currentFocus}${historySections}`;
365
+ }
366
+ else {
367
+ projectContextContent = currentFocus;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ catch (e) {
373
+ logger?.error(`[PD:Prompt] Failed to read CURRENT_FOCUS: ${String(e)}`);
374
+ }
375
+ }
250
376
  }
251
- if (hygiene.persistenceCount === 0 && trigger === 'user') {
252
- dynamicContext += `⚠️ ADVISORY: You haven't persisted any state today. To prevent "Goldfish Memory", consider updating PLAN.md or writing notes to memory/ if this session is becoming complex.\n`;
377
+ // Build appendSystemContext with recency effect
378
+ // Content order (most important last): project_context -> reflection_log -> thinking_os -> principles
379
+ const appendParts = [];
380
+ // 1. Project Context (lowest priority, goes first)
381
+ if (projectContextContent) {
382
+ appendParts.push(`<project_context>\n${projectContextContent}\n</project_context>`);
253
383
  }
254
- // 4. 使用命名空间前缀 (pd:internal_context)
255
- if (dynamicContext.trim()) {
256
- prependContext = `\n<pd:internal_context>\n${dynamicContext.trim()}\n</pd:internal_context>\n` + prependContext;
384
+ // 2. Reflection Log
385
+ if (reflectionLogContent) {
386
+ appendParts.push(`<reflection_log>\n${reflectionLogContent}\n</reflection_log>`);
257
387
  }
258
- // 注入进化指令(如果有),放在 prependContext 最前面(高优先级)
259
- if (evolutionDirective) {
260
- prependContext = evolutionDirective + prependContext;
388
+ // 3. Thinking OS (configurable)
389
+ if (thinkingOsContent) {
390
+ appendParts.push(`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`);
261
391
  }
262
- // ═══ SIZE GUARD: Prevent token explosion ═══
392
+ // 4. Principles (always on, highest priority, goes last for recency effect)
393
+ if (principlesContent) {
394
+ appendParts.push(`<core_principles>\n${principlesContent}\n</core_principles>`);
395
+ }
396
+ if (appendParts.length > 0) {
397
+ appendSystemContext = `
398
+ ## 📋 CONTEXT SECTIONS (Priority: Low → High)
399
+
400
+ The sections below are ordered by priority. When conflicts arise, **later sections override earlier ones**.
401
+
402
+ `;
403
+ appendSystemContext += appendParts.join('\n\n');
404
+ appendSystemContext += `
405
+
406
+ ---
407
+
408
+ **⚠️ EXECUTION RULES** (Priority: Low → High):
409
+ - \`<project_context>\` - Current priorities (can be overridden)
410
+ - \`<reflection_log>\` - Past lessons (inform your approach)
411
+ - \`<thinking_os>\` - Thinking models (guide your reasoning)
412
+ - \`<core_principles>\` - Core rules (NON-NEGOTIABLE, highest priority)
413
+
414
+ **Remember**: You are the Spicy Evolver. You despise entropy. You evolve through pain.
415
+
416
+ ${attitudeDirective}
417
+ `;
418
+ }
419
+ // ═══ 8. SIZE GUARD ═══
420
+ // Truncation happens within appendSystemContext (not prependContext)
263
421
  const totalSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
264
422
  const MAX_SIZE = 10000;
265
423
  if (totalSize > MAX_SIZE) {
266
424
  const originalSize = totalSize;
267
- // Truncate <project_context> to first 50 lines
268
- const projectContextMatch = prependContext.match(/(<project_context>[\s\S]*?<\/project_context>)/);
269
- if (projectContextMatch) {
270
- const originalBlock = projectContextMatch[1];
271
- const lines = originalBlock.split('\n');
272
- if (lines.length > 50) {
273
- const truncatedBlock = lines.slice(0, 50).join('\n') + '\n...[truncated]';
274
- prependContext = prependContext.replace(originalBlock, truncatedBlock);
425
+ const truncationLog = [];
426
+ // 1. Truncate project_context in appendSystemContext
427
+ if (projectContextContent && appendSystemContext.includes('<project_context>')) {
428
+ const lines = projectContextContent.split('\n');
429
+ if (lines.length > 20) {
430
+ const truncated = lines.slice(0, 20).join('\n') + '\n...[truncated]';
431
+ appendSystemContext = appendSystemContext.replace(`<project_context>\n${projectContextContent}\n</project_context>`, `<project_context>\n${truncated}\n</project_context>`);
432
+ truncationLog.push('project_context');
275
433
  }
276
434
  }
277
- const newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
278
- logger?.warn(`[PD:Prompt] Injection size exceeded: ${originalSize} chars (limit: ${MAX_SIZE}), truncated to ${newSize} chars (${newSize - originalSize} saved)`);
435
+ // 2. Truncate reflection_log if still over limit
436
+ let newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
437
+ if (newSize > MAX_SIZE && reflectionLogContent && appendSystemContext.includes('<reflection_log>')) {
438
+ const lines = reflectionLogContent.split('\n');
439
+ if (lines.length > 30) {
440
+ const truncated = lines.slice(0, 30).join('\n') + '\n...[truncated]';
441
+ appendSystemContext = appendSystemContext.replace(`<reflection_log>\n${reflectionLogContent}\n</reflection_log>`, `<reflection_log>\n${truncated}\n</reflection_log>`);
442
+ truncationLog.push('reflection_log');
443
+ }
444
+ }
445
+ // 3. Final check
446
+ newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
447
+ if (newSize > MAX_SIZE) {
448
+ // NOTE: We still return the content even if over limit, as truncating more
449
+ // could lose critical context like principles or evolution directives.
450
+ logger?.error(`[PD:Prompt] Cannot reduce injection size below limit. Current: ${newSize}, Limit: ${MAX_SIZE}`);
451
+ }
452
+ logger?.warn(`[PD:Prompt] Injection size exceeded: ${originalSize} chars (limit: ${MAX_SIZE}), truncated: ${truncationLog.join(', ') || 'none'}, new size: ${newSize} chars`);
279
453
  }
280
454
  return {
281
455
  prependSystemContext,
@@ -1,2 +1,9 @@
1
- import { PluginHookSubagentEndedEvent, PluginHookAgentContext } from '../openclaw-sdk.js';
2
- export declare function handleSubagentEnded(event: PluginHookSubagentEndedEvent, ctx: PluginHookAgentContext): Promise<void>;
1
+ import type { PluginHookSubagentEndedEvent, PluginHookSubagentContext } from '../openclaw-sdk.js';
2
+ import { type EmpathyObserverApi } from '../service/empathy-observer-manager.js';
3
+ type SubagentEndedHookContext = PluginHookSubagentContext & {
4
+ api?: EmpathyObserverApi;
5
+ workspaceDir?: string;
6
+ sessionId?: string;
7
+ };
8
+ export declare function handleSubagentEnded(event: PluginHookSubagentEndedEvent, ctx: SubagentEndedHookContext): Promise<void>;
9
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { writePainFlag } from '../core/pain.js';
2
2
  import { WorkspaceContext } from '../core/workspace-context.js';
3
+ import { empathyObserverManager } from '../service/empathy-observer-manager.js';
3
4
  import * as fs from 'fs';
4
5
  export async function handleSubagentEnded(event, ctx) {
5
6
  const { outcome, targetSessionKey } = event;
@@ -7,6 +8,11 @@ export async function handleSubagentEnded(event, ctx) {
7
8
  if (!workspaceDir)
8
9
  return;
9
10
  const wctx = WorkspaceContext.fromHookContext(ctx);
11
+ // Empathy observer subagent session is handled by sidecar manager
12
+ if (targetSessionKey?.startsWith('empathy_obs:')) {
13
+ await empathyObserverManager.reap(ctx.api, targetSessionKey, workspaceDir);
14
+ return;
15
+ }
10
16
  const config = wctx.config;
11
17
  // 1. Autonomous Pain Capture: If subagent failed, record pain
12
18
  if (outcome === 'error' || outcome === 'timeout') {
@@ -35,8 +41,13 @@ export async function handleSubagentEnded(event, ctx) {
35
41
  // Find in_progress tasks
36
42
  const inProgressTasks = queue.filter((t) => t.status === 'in_progress');
37
43
  if (inProgressTasks.length > 0) {
38
- // Sort by timestamp to find the oldest one
39
- const oldestTask = inProgressTasks.sort((a, b) => new Date(a.enqueued_at).getTime() - new Date(b.enqueued_at).getTime())[0];
44
+ const resolveTaskTime = (task) => {
45
+ const raw = task?.enqueued_at || task?.timestamp;
46
+ const ts = new Date(raw).getTime();
47
+ return Number.isFinite(ts) ? ts : Number.MAX_SAFE_INTEGER;
48
+ };
49
+ // Sort by enqueue timestamp (fallback to legacy timestamp) to find the oldest task
50
+ const oldestTask = inProgressTasks.sort((a, b) => resolveTaskTime(a) - resolveTaskTime(b))[0];
40
51
  // Mark as completed
41
52
  const taskIndex = queue.findIndex((t) => t.id === oldestTask.id);
42
53
  if (taskIndex !== -1) {