principles-disciple 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants/tools.d.ts +17 -0
- package/dist/constants/tools.js +54 -0
- package/dist/core/event-log.d.ts +4 -0
- package/dist/core/event-log.js +62 -118
- package/dist/core/evolution-engine.d.ts +3 -4
- package/dist/core/evolution-engine.js +60 -118
- package/dist/core/migration.js +1 -1
- package/dist/core/session-tracker.d.ts +1 -0
- package/dist/core/session-tracker.js +39 -11
- package/dist/core/trust-engine.d.ts +1 -2
- package/dist/core/trust-engine.js +4 -23
- package/dist/hooks/gate.js +4 -25
- package/dist/hooks/prompt.js +12 -1
- package/dist/hooks/subagent.js +109 -63
- package/dist/service/control-ui-query-service.d.ts +2 -0
- package/dist/service/control-ui-query-service.js +2 -0
- package/dist/service/evolution-worker.d.ts +12 -8
- package/dist/service/evolution-worker.js +153 -123
- package/dist/service/runtime-summary-service.d.ts +4 -0
- package/dist/service/runtime-summary-service.js +43 -4
- package/dist/tools/agent-spawn.js +23 -0
- package/dist/utils/file-lock.d.ts +7 -0
- package/dist/utils/file-lock.js +66 -27
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -1
|
@@ -4,13 +4,20 @@ import { SystemLogger } from './system-logger.js';
|
|
|
4
4
|
const sessions = new Map();
|
|
5
5
|
/** Directory for persisting session state */
|
|
6
6
|
let persistDir = null;
|
|
7
|
-
/** Debounce
|
|
8
|
-
|
|
7
|
+
/** Debounce timers for persistence, one per session */
|
|
8
|
+
const persistTimers = new Map();
|
|
9
9
|
function logSessionTrackerWarning(message, error) {
|
|
10
10
|
const detail = error instanceof Error ? error.message : error ? String(error) : '';
|
|
11
11
|
const suffix = detail ? `: ${detail}` : '';
|
|
12
12
|
console.warn(`[PD:SessionTracker] ${message}${suffix}`);
|
|
13
13
|
}
|
|
14
|
+
function touchActivity(state, kind = 'general') {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
state.lastActivityAt = now;
|
|
17
|
+
if (kind === 'control') {
|
|
18
|
+
state.lastControlActivityAt = now;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
/**
|
|
15
22
|
* Initialize persistence for session state.
|
|
16
23
|
* Call this once during plugin startup.
|
|
@@ -82,18 +89,24 @@ function persistSession(state) {
|
|
|
82
89
|
* Schedule persistence with debounce.
|
|
83
90
|
*/
|
|
84
91
|
function schedulePersistence(state) {
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
const existing = persistTimers.get(state.sessionId);
|
|
93
|
+
if (existing) {
|
|
94
|
+
clearTimeout(existing);
|
|
87
95
|
}
|
|
88
|
-
|
|
96
|
+
const timer = setTimeout(() => {
|
|
89
97
|
persistSession(state);
|
|
90
|
-
|
|
98
|
+
persistTimers.delete(state.sessionId);
|
|
91
99
|
}, 1000); // 1 second debounce
|
|
100
|
+
persistTimers.set(state.sessionId, timer);
|
|
92
101
|
}
|
|
93
102
|
/**
|
|
94
103
|
* Force persist all sessions immediately.
|
|
95
104
|
*/
|
|
96
105
|
export function flushAllSessions() {
|
|
106
|
+
for (const timer of persistTimers.values()) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
persistTimers.clear();
|
|
97
110
|
for (const state of sessions.values()) {
|
|
98
111
|
persistSession(state);
|
|
99
112
|
}
|
|
@@ -108,6 +121,7 @@ function getOrCreateSession(sessionId, workspaceDir) {
|
|
|
108
121
|
llmTurns: 0,
|
|
109
122
|
blockedAttempts: 0,
|
|
110
123
|
lastActivityAt: Date.now(),
|
|
124
|
+
lastControlActivityAt: Date.now(),
|
|
111
125
|
totalInputTokens: 0,
|
|
112
126
|
totalOutputTokens: 0,
|
|
113
127
|
cacheHits: 0,
|
|
@@ -141,13 +155,13 @@ export function trackToolRead(sessionId, filePath, workspaceDir) {
|
|
|
141
155
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
142
156
|
const normalizedPath = path.posix.normalize(filePath.replace(/\\/g, '/'));
|
|
143
157
|
state.toolReadsByFile[normalizedPath] = (state.toolReadsByFile[normalizedPath] || 0) + 1;
|
|
144
|
-
state
|
|
158
|
+
touchActivity(state);
|
|
145
159
|
return state;
|
|
146
160
|
}
|
|
147
161
|
export function trackLlmOutput(sessionId, usage, config, workspaceDir) {
|
|
148
162
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
149
163
|
state.llmTurns += 1;
|
|
150
|
-
state
|
|
164
|
+
touchActivity(state);
|
|
151
165
|
if (usage) {
|
|
152
166
|
state.totalInputTokens += usage.input || 0;
|
|
153
167
|
state.totalOutputTokens += usage.output || 0;
|
|
@@ -195,7 +209,7 @@ export function trackFriction(sessionId, deltaF, hash, workspaceDir, options) {
|
|
|
195
209
|
state.currentGfi = (state.currentGfi || 0) + addedFriction;
|
|
196
210
|
const sourceKey = options?.source || (hash ? `unattributed:${hash}` : 'unattributed:unknown');
|
|
197
211
|
ledger[sourceKey] = (ledger[sourceKey] || 0) + addedFriction;
|
|
198
|
-
state
|
|
212
|
+
touchActivity(state, 'control');
|
|
199
213
|
SystemLogger.log(state.workspaceDir, 'GFI_INC', `Friction added: +${addedFriction.toFixed(1)} (Base: ${deltaF}, Mult: ${multiplier.toFixed(2)}). Total GFI: ${state.currentGfi.toFixed(1)}`);
|
|
200
214
|
// Update daily stats
|
|
201
215
|
state.dailyToolFailures++;
|
|
@@ -228,6 +242,7 @@ export function resetFriction(sessionId, workspaceDir, options) {
|
|
|
228
242
|
state.lastErrorSource = '';
|
|
229
243
|
}
|
|
230
244
|
}
|
|
245
|
+
touchActivity(state, 'control');
|
|
231
246
|
schedulePersistence(state);
|
|
232
247
|
return state;
|
|
233
248
|
}
|
|
@@ -239,6 +254,7 @@ export function resetFriction(sessionId, workspaceDir, options) {
|
|
|
239
254
|
state.lastErrorSource = '';
|
|
240
255
|
state.consecutiveErrors = 0;
|
|
241
256
|
state.lastErrorHash = '';
|
|
257
|
+
touchActivity(state, 'control');
|
|
242
258
|
// Schedule persistence
|
|
243
259
|
schedulePersistence(state);
|
|
244
260
|
return state;
|
|
@@ -250,6 +266,7 @@ export function resetFriction(sessionId, workspaceDir, options) {
|
|
|
250
266
|
export function recordThinkingCheckpoint(sessionId, workspaceDir) {
|
|
251
267
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
252
268
|
state.lastThinkingTimestamp = Date.now();
|
|
269
|
+
touchActivity(state, 'control');
|
|
253
270
|
SystemLogger.log(state.workspaceDir, 'THINKING_CHECKPOINT', `Deep thinking recorded at ${new Date(state.lastThinkingTimestamp).toISOString()}`);
|
|
254
271
|
schedulePersistence(state);
|
|
255
272
|
return state;
|
|
@@ -269,13 +286,14 @@ export function hasRecentThinking(sessionId, windowMs = 5 * 60 * 1000) {
|
|
|
269
286
|
export function trackBlock(sessionId) {
|
|
270
287
|
const state = getOrCreateSession(sessionId);
|
|
271
288
|
state.blockedAttempts += 1;
|
|
272
|
-
state
|
|
289
|
+
touchActivity(state, 'control');
|
|
290
|
+
schedulePersistence(state);
|
|
273
291
|
return state;
|
|
274
292
|
}
|
|
275
293
|
export function setInjectedProbationIds(sessionId, ids, workspaceDir) {
|
|
276
294
|
const state = getOrCreateSession(sessionId, workspaceDir);
|
|
277
295
|
state.injectedProbationIds = [...ids];
|
|
278
|
-
state
|
|
296
|
+
touchActivity(state, 'control');
|
|
279
297
|
schedulePersistence(state);
|
|
280
298
|
return state;
|
|
281
299
|
}
|
|
@@ -300,6 +318,11 @@ export function listSessions(workspaceDir) {
|
|
|
300
318
|
}));
|
|
301
319
|
}
|
|
302
320
|
export function clearSession(sessionId) {
|
|
321
|
+
const timer = persistTimers.get(sessionId);
|
|
322
|
+
if (timer) {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
persistTimers.delete(sessionId);
|
|
325
|
+
}
|
|
303
326
|
sessions.delete(sessionId);
|
|
304
327
|
}
|
|
305
328
|
// Memory cleanup for abandoned sessions (older than 2 hours)
|
|
@@ -307,6 +330,11 @@ export function garbageCollectSessions() {
|
|
|
307
330
|
const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
|
|
308
331
|
for (const [id, state] of sessions.entries()) {
|
|
309
332
|
if (state.lastActivityAt < twoHoursAgo) {
|
|
333
|
+
const timer = persistTimers.get(id);
|
|
334
|
+
if (timer) {
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
persistTimers.delete(id);
|
|
337
|
+
}
|
|
310
338
|
sessions.delete(id);
|
|
311
339
|
// Also delete persisted file
|
|
312
340
|
if (persistDir) {
|
|
@@ -21,8 +21,7 @@ export interface TrustScorecard {
|
|
|
21
21
|
reward_policy?: 'frozen_all_positive' | 'frozen_atomic_positive_keep_plan_ready';
|
|
22
22
|
}
|
|
23
23
|
export type TrustStage = 1 | 2 | 3 | 4;
|
|
24
|
-
export declare const EXPLORATORY_TOOLS: string
|
|
25
|
-
export declare const CONSTRUCTIVE_TOOLS: string[];
|
|
24
|
+
export declare const EXPLORATORY_TOOLS: Set<string>;
|
|
26
25
|
export declare class TrustEngine {
|
|
27
26
|
private scorecard;
|
|
28
27
|
private workspaceDir;
|
|
@@ -8,27 +8,8 @@ import { EventLogService } from './event-log.js';
|
|
|
8
8
|
import { resolvePdPath } from './paths.js';
|
|
9
9
|
import { ConfigService } from './config-service.js';
|
|
10
10
|
import { TrajectoryRegistry } from './trajectory.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'read', 'read_file', 'read_many_files', 'image_read',
|
|
14
|
-
// 搜索和列表
|
|
15
|
-
'search_file_content', 'grep', 'grep_search', 'list_directory', 'ls', 'glob',
|
|
16
|
-
// Web
|
|
17
|
-
'web_fetch', 'web_search',
|
|
18
|
-
// 用户交互
|
|
19
|
-
'ask_user', 'ask_user_question',
|
|
20
|
-
// LSP
|
|
21
|
-
'lsp_hover', 'lsp_goto_definition', 'lsp_find_references',
|
|
22
|
-
// 内存和状态
|
|
23
|
-
'memory_recall', 'save_memory', 'todo_read', 'todo_write',
|
|
24
|
-
// 状态查询
|
|
25
|
-
'pd-status', 'trust', 'report',
|
|
26
|
-
];
|
|
27
|
-
export const CONSTRUCTIVE_TOOLS = [
|
|
28
|
-
'write', 'write_file', 'edit', 'edit_file', 'replace', 'apply_patch',
|
|
29
|
-
'insert', 'patch', 'delete_file', 'move_file', 'run_shell_command',
|
|
30
|
-
'pd_run_worker', 'sessions_spawn', 'evolve-task', 'init-strategy'
|
|
31
|
-
];
|
|
11
|
+
import { EXPLORATORY_TOOLS as SHARED_EXPLORATORY_TOOLS } from '../constants/tools.js';
|
|
12
|
+
export const EXPLORATORY_TOOLS = new Set(SHARED_EXPLORATORY_TOOLS);
|
|
32
13
|
const LEGACY_TRUST_REWARD_POLICY = 'frozen_all_positive';
|
|
33
14
|
export class TrustEngine {
|
|
34
15
|
scorecard;
|
|
@@ -130,7 +111,7 @@ export class TrustEngine {
|
|
|
130
111
|
recordSuccess(reason, context, isSubagent = false) {
|
|
131
112
|
const toolName = context?.toolName;
|
|
132
113
|
// 1. Check if this is an exploratory tool success
|
|
133
|
-
const isExploratory = toolName
|
|
114
|
+
const isExploratory = toolName ? EXPLORATORY_TOOLS.has(toolName) : false;
|
|
134
115
|
if (reason === 'tool_success' && isExploratory) {
|
|
135
116
|
this.scorecard.exploratory_failure_streak = 0;
|
|
136
117
|
this.touchScorecard();
|
|
@@ -147,7 +128,7 @@ export class TrustEngine {
|
|
|
147
128
|
const penalties = settings.penalties;
|
|
148
129
|
const toolName = context?.toolName;
|
|
149
130
|
// 1. Classification: Is this an exploratory failure?
|
|
150
|
-
const isExploratory = toolName
|
|
131
|
+
const isExploratory = toolName ? EXPLORATORY_TOOLS.has(toolName) : false;
|
|
151
132
|
// 2. Cold start grace (only for non-risky actions)
|
|
152
133
|
if (type !== 'risky' && this.isColdStart() && (this.scorecard.grace_failures_remaining || 0) > 0) {
|
|
153
134
|
this.scorecard.grace_failures_remaining = (this.scorecard.grace_failures_remaining || 0) - 1;
|
package/dist/hooks/gate.js
CHANGED
|
@@ -8,32 +8,14 @@ import { assessRiskLevel, estimateLineChanges } from '../core/risk-calculator.js
|
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
9
|
import { checkEvolutionGate } from '../core/evolution-engine.js';
|
|
10
10
|
import { EventLogService } from '../core/event-log.js';
|
|
11
|
+
import { AGENT_TOOLS, BASH_TOOLS_SET, HIGH_RISK_TOOLS, LOW_RISK_WRITE_TOOLS, WRITE_TOOLS, } from '../constants/tools.js';
|
|
11
12
|
// ═══ GFI Gate Tool Tiers ═══
|
|
12
13
|
// TIER 0: 只读工具 - 永不拦截
|
|
13
|
-
const READ_ONLY_TOOLS = new Set([
|
|
14
|
-
'read', 'read_file', 'read_many_files', 'image_read',
|
|
15
|
-
'search_file_content', 'grep', 'grep_search', 'list_directory', 'ls', 'glob',
|
|
16
|
-
'lsp_hover', 'lsp_goto_definition', 'lsp_find_references',
|
|
17
|
-
'web_fetch', 'web_search', 'ref_search_documentation', 'ref_read_url',
|
|
18
|
-
'resolve-library-id', 'get-library-docs',
|
|
19
|
-
'todo_read', 'save_memory',
|
|
20
|
-
'deep_reflect',
|
|
21
|
-
]);
|
|
22
14
|
// TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
|
|
23
15
|
// 注意:pd_run_worker、sessions_spawn、task 是 Agent 派生工具,不应被 GFI Gate 拦截
|
|
24
16
|
// 它们属于 AGENT_TOOLS,在早期过滤后直接放行
|
|
25
|
-
const LOW_RISK_WRITE_TOOLS = new Set([
|
|
26
|
-
'write', 'write_file',
|
|
27
|
-
'edit', 'edit_file', 'replace', 'apply_patch', 'insert', 'patch',
|
|
28
|
-
]);
|
|
29
17
|
// TIER 2: 高风险操作 - GFI >= high_risk_block 时拦截
|
|
30
|
-
const HIGH_RISK_TOOLS = new Set([
|
|
31
|
-
'delete_file', 'move_file',
|
|
32
|
-
]);
|
|
33
18
|
// TIER 3: Bash 命令 - 根据内容判断
|
|
34
|
-
const BASH_TOOLS_SET = new Set([
|
|
35
|
-
'bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd',
|
|
36
|
-
]);
|
|
37
19
|
/**
|
|
38
20
|
* 分析 Bash 命令风险等级
|
|
39
21
|
*/
|
|
@@ -118,12 +100,9 @@ function calculateDynamicThreshold(baseThreshold, trustStage, lineChanges, confi
|
|
|
118
100
|
export function handleBeforeToolCall(event, ctx) {
|
|
119
101
|
const logger = ctx.logger || console;
|
|
120
102
|
// 1. Identify tool type
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const isBash = BASH_TOOLS.includes(event.toolName);
|
|
125
|
-
const isWriteTool = WRITE_TOOLS.includes(event.toolName);
|
|
126
|
-
const isAgentTool = AGENT_TOOLS.includes(event.toolName);
|
|
103
|
+
const isBash = BASH_TOOLS_SET.has(event.toolName);
|
|
104
|
+
const isWriteTool = WRITE_TOOLS.has(event.toolName);
|
|
105
|
+
const isAgentTool = AGENT_TOOLS.has(event.toolName);
|
|
127
106
|
// Profile loaded first for config-driven behavior (see below)
|
|
128
107
|
if (!ctx.workspaceDir || (!isWriteTool && !isBash && !isAgentTool)) {
|
|
129
108
|
return;
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { getSession, resetFriction } from '../core/session-tracker.js';
|
|
3
|
+
import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds } from '../core/session-tracker.js';
|
|
4
4
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
5
5
|
import { defaultContextConfig } from '../types.js';
|
|
6
6
|
import { extractSummary, getHistoryVersions } from '../core/focus-history.js';
|
|
@@ -575,6 +575,14 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
575
575
|
const reducer = wctx.evolutionReducer;
|
|
576
576
|
const active = reducer.getActivePrinciples().slice(-3);
|
|
577
577
|
const probation = reducer.getProbationPrinciples().slice(0, 5);
|
|
578
|
+
if (ctx.sessionId) {
|
|
579
|
+
if (probation.length > 0) {
|
|
580
|
+
setInjectedProbationIds(ctx.sessionId, probation.map((p) => p.id), workspaceDir);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
clearInjectedProbationIds(ctx.sessionId, workspaceDir);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
578
586
|
if (active.length > 0 || probation.length > 0) {
|
|
579
587
|
const lines = [];
|
|
580
588
|
if (active.length > 0) {
|
|
@@ -593,6 +601,9 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
593
601
|
}
|
|
594
602
|
}
|
|
595
603
|
catch (e) {
|
|
604
|
+
if (ctx.sessionId) {
|
|
605
|
+
clearInjectedProbationIds(ctx.sessionId, workspaceDir);
|
|
606
|
+
}
|
|
596
607
|
logger?.warn?.(`[PD:Prompt] Failed to load evolution principles: ${String(e)}`);
|
|
597
608
|
}
|
|
598
609
|
// Build appendSystemContext with recency effect
|
package/dist/hooks/subagent.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
1
2
|
import { writePainFlag } from '../core/pain.js';
|
|
2
3
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
3
4
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
4
|
-
import { acquireQueueLock
|
|
5
|
-
|
|
5
|
+
import { acquireQueueLock } from '../service/evolution-worker.js';
|
|
6
|
+
const COMPLETION_RETRY_DELAY_MS = 250;
|
|
7
|
+
const COMPLETION_MAX_RETRIES = 3;
|
|
8
|
+
const COMPLETION_RETRY_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for retry entries
|
|
9
|
+
const DIAGNOSTICIAN_SESSION_PREFIX = 'agent:diagnostician:';
|
|
10
|
+
const completionRetryCounts = new Map();
|
|
11
|
+
// Cleanup expired retry entries periodically
|
|
12
|
+
function cleanupExpiredRetryEntries() {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
for (const [key, value] of completionRetryCounts.entries()) {
|
|
15
|
+
if (now > value.expires) {
|
|
16
|
+
completionRetryCounts.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
6
20
|
function emitSubagentPainEvent(wctx, payload) {
|
|
7
21
|
try {
|
|
8
22
|
wctx.evolutionReducer.emitSync({
|
|
@@ -22,19 +36,73 @@ function emitSubagentPainEvent(wctx, payload) {
|
|
|
22
36
|
console.warn(`[PD:Subagent] failed to emit evolution event: ${String(e)}`);
|
|
23
37
|
}
|
|
24
38
|
}
|
|
39
|
+
function isDiagnosticianSession(targetSessionKey) {
|
|
40
|
+
return typeof targetSessionKey === 'string' && targetSessionKey.startsWith(DIAGNOSTICIAN_SESSION_PREFIX);
|
|
41
|
+
}
|
|
42
|
+
function cleanupPainFlagForTask(wctx, completedTaskId, queue) {
|
|
43
|
+
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
44
|
+
try {
|
|
45
|
+
const painData = fs.readFileSync(painFlagPath, 'utf8');
|
|
46
|
+
const taskIdMatch = painData.match(/^task_id:\s*(.+)$/m);
|
|
47
|
+
const painTaskId = taskIdMatch?.[1]?.trim();
|
|
48
|
+
const hasQueuedStatus = painData.includes('status: queued');
|
|
49
|
+
const hasRemainingActiveTasks = queue.some((task) => task?.status === 'pending' || task?.status === 'in_progress');
|
|
50
|
+
if (!hasQueuedStatus)
|
|
51
|
+
return;
|
|
52
|
+
if (painTaskId) {
|
|
53
|
+
if (painTaskId === completedTaskId) {
|
|
54
|
+
fs.unlinkSync(painFlagPath);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Legacy fallback: only clear an untagged queued pain flag when there are
|
|
59
|
+
// no active queue entries left. This avoids unrelated diagnostician runs
|
|
60
|
+
// from deleting a queued flag that belongs to another task.
|
|
61
|
+
if (!hasRemainingActiveTasks) {
|
|
62
|
+
fs.unlinkSync(painFlagPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (e.code === 'ENOENT')
|
|
67
|
+
return; // File doesn't exist, nothing to clean up
|
|
68
|
+
console.error(`[PD:Subagent] Failed to cleanup pain flag: ${String(e)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function getCompletionRetryKey(workspaceDir, targetSessionKey) {
|
|
72
|
+
return `${workspaceDir}::${targetSessionKey}`;
|
|
73
|
+
}
|
|
74
|
+
function scheduleCompletionRetry(event, ctx, attempt) {
|
|
75
|
+
const workspaceDir = ctx.workspaceDir;
|
|
76
|
+
const targetSessionKey = event.targetSessionKey;
|
|
77
|
+
if (!workspaceDir || !targetSessionKey || attempt >= COMPLETION_MAX_RETRIES) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
cleanupExpiredRetryEntries();
|
|
81
|
+
const retryKey = getCompletionRetryKey(workspaceDir, targetSessionKey);
|
|
82
|
+
completionRetryCounts.set(retryKey, {
|
|
83
|
+
count: attempt + 1,
|
|
84
|
+
expires: Date.now() + COMPLETION_RETRY_TTL_MS
|
|
85
|
+
});
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
void handleSubagentEnded(event, ctx).finally(() => {
|
|
88
|
+
const entry = completionRetryCounts.get(retryKey);
|
|
89
|
+
if (!entry || entry.count <= attempt + 1) {
|
|
90
|
+
completionRetryCounts.delete(retryKey);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}, COMPLETION_RETRY_DELAY_MS);
|
|
94
|
+
}
|
|
25
95
|
export async function handleSubagentEnded(event, ctx) {
|
|
26
96
|
const { outcome, targetSessionKey } = event;
|
|
27
97
|
const workspaceDir = ctx.workspaceDir;
|
|
28
98
|
if (!workspaceDir)
|
|
29
99
|
return;
|
|
30
100
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
31
|
-
// Empathy observer subagent session is handled by sidecar manager
|
|
32
101
|
if (targetSessionKey?.startsWith('empathy_obs:')) {
|
|
33
102
|
await empathyObserverManager.reap(ctx.api, targetSessionKey, workspaceDir);
|
|
34
103
|
return;
|
|
35
104
|
}
|
|
36
105
|
const config = wctx.config;
|
|
37
|
-
// 1. Autonomous Pain Capture: If subagent failed, record pain
|
|
38
106
|
if (outcome === 'error' || outcome === 'timeout') {
|
|
39
107
|
const scoreSettings = config.get('scores');
|
|
40
108
|
const score = outcome === 'error' ? scoreSettings.subagent_error_penalty : scoreSettings.subagent_timeout_penalty;
|
|
@@ -53,70 +121,48 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
53
121
|
sessionId: ctx.sessionId,
|
|
54
122
|
});
|
|
55
123
|
}
|
|
56
|
-
// 2. Loop Closure: Clean up evolution queue if any subagent finished successfully
|
|
57
124
|
if (outcome === 'ok' || outcome === 'deleted') {
|
|
58
|
-
// ── Trust Engine: Record success using V2 API ──
|
|
59
125
|
wctx.trust.recordSuccess('subagent_success', {
|
|
60
126
|
sessionId: ctx.sessionId,
|
|
61
127
|
api: ctx.api
|
|
62
128
|
}, true);
|
|
63
|
-
|
|
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
|
-
oldestTaskIndex = index;
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
if (oldestTaskIndex !== -1) {
|
|
93
|
-
queue[oldestTaskIndex].status = 'completed';
|
|
94
|
-
queue[oldestTaskIndex].completed_at = new Date().toISOString();
|
|
95
|
-
changed = true;
|
|
96
|
-
// Clean up the .pain_flag if it was queued, to reset the environment
|
|
97
|
-
const painFlagPath = wctx.resolve('PAIN_FLAG');
|
|
98
|
-
if (fs.existsSync(painFlagPath)) {
|
|
99
|
-
try {
|
|
100
|
-
const painData = fs.readFileSync(painFlagPath, 'utf8');
|
|
101
|
-
if (painData.includes('status: queued')) {
|
|
102
|
-
fs.unlinkSync(painFlagPath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
console.error(`[PD:Subagent] Failed to cleanup pain flag: ${String(e)}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (changed) {
|
|
111
|
-
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (e) {
|
|
115
|
-
console.error(`[PD:Subagent] Failed to update evolution queue: ${String(e)}`);
|
|
116
|
-
}
|
|
117
|
-
finally {
|
|
118
|
-
releaseLock();
|
|
119
|
-
}
|
|
129
|
+
}
|
|
130
|
+
if ((outcome !== 'ok' && outcome !== 'deleted') || !isDiagnosticianSession(targetSessionKey)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const queuePath = wctx.resolve('EVOLUTION_QUEUE');
|
|
134
|
+
if (!fs.existsSync(queuePath))
|
|
135
|
+
return;
|
|
136
|
+
const retryKey = getCompletionRetryKey(workspaceDir, targetSessionKey);
|
|
137
|
+
const retryEntry = completionRetryCounts.get(retryKey);
|
|
138
|
+
const attempt = retryEntry?.count || 0;
|
|
139
|
+
let releaseLock = null;
|
|
140
|
+
try {
|
|
141
|
+
releaseLock = await acquireQueueLock(queuePath, console);
|
|
142
|
+
const queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
143
|
+
let completedTaskId = null;
|
|
144
|
+
const matchedTask = queue.find((task) => task?.status === 'in_progress'
|
|
145
|
+
&& typeof task?.assigned_session_key === 'string'
|
|
146
|
+
&& task.assigned_session_key === targetSessionKey);
|
|
147
|
+
if (matchedTask) {
|
|
148
|
+
matchedTask.status = 'completed';
|
|
149
|
+
matchedTask.completed_at = new Date().toISOString();
|
|
150
|
+
delete matchedTask.assigned_session_key;
|
|
151
|
+
completedTaskId = matchedTask.id;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.warn(`[PD:Subagent] No in-progress evolution task matched subagent session ${targetSessionKey}`);
|
|
120
155
|
}
|
|
156
|
+
if (completedTaskId) {
|
|
157
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2), 'utf8');
|
|
158
|
+
cleanupPainFlagForTask(wctx, completedTaskId, queue);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
console.error(`[PD:Subagent] Failed to update evolution queue: ${String(e)}`);
|
|
163
|
+
scheduleCompletionRetry(event, ctx, attempt);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
releaseLock?.();
|
|
121
167
|
}
|
|
122
168
|
}
|
|
@@ -2,6 +2,8 @@ export interface OverviewResponse {
|
|
|
2
2
|
workspaceDir: string;
|
|
3
3
|
generatedAt: string;
|
|
4
4
|
dataFreshness: string | null;
|
|
5
|
+
dataSource: 'trajectory_db_analytics';
|
|
6
|
+
runtimeControlPlaneSource: 'pd_evolution_status';
|
|
5
7
|
summary: {
|
|
6
8
|
repeatErrorRate: number;
|
|
7
9
|
userCorrectionRate: number;
|
|
@@ -98,6 +98,8 @@ export class ControlUiQueryService {
|
|
|
98
98
|
workspaceDir: this.workspaceDir,
|
|
99
99
|
generatedAt: new Date().toISOString(),
|
|
100
100
|
dataFreshness: stats.lastIngestAt,
|
|
101
|
+
dataSource: 'trajectory_db_analytics',
|
|
102
|
+
runtimeControlPlaneSource: 'pd_evolution_status',
|
|
101
103
|
summary: {
|
|
102
104
|
repeatErrorRate: roundRate(Number(failureStats.repeated_failures), Number(failureStats.total_failures)),
|
|
103
105
|
userCorrectionRate: roundRate(correctionTotal, stats.userTurns),
|
|
@@ -7,6 +7,10 @@ export interface EvolutionQueueItem {
|
|
|
7
7
|
source: string;
|
|
8
8
|
reason: string;
|
|
9
9
|
timestamp: string;
|
|
10
|
+
enqueued_at?: string;
|
|
11
|
+
started_at?: string;
|
|
12
|
+
completed_at?: string;
|
|
13
|
+
assigned_session_key?: string;
|
|
10
14
|
trigger_text_preview?: string;
|
|
11
15
|
status: 'pending' | 'in_progress' | 'completed';
|
|
12
16
|
}
|
|
@@ -19,12 +23,8 @@ export declare function createEvolutionTaskId(source: string, score: number, pre
|
|
|
19
23
|
export declare function shouldTrackPainCandidate(text: string): boolean;
|
|
20
24
|
export declare function createPainCandidateFingerprint(text: string): string;
|
|
21
25
|
export declare function summarizePainCandidateSample(text: string): string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* Returns a release function. Uses 'wx' flag for atomic exclusive create.
|
|
25
|
-
* Detects stale locks by checking PID and mtime.
|
|
26
|
-
*/
|
|
27
|
-
export declare function acquireQueueLock(lockPath: string, logger: any): (() => void) | null;
|
|
26
|
+
export declare function acquireQueueLock(resourcePath: string, logger: any, lockSuffix?: string): Promise<() => void>;
|
|
27
|
+
export declare function extractEvolutionTaskId(task: string): string | null;
|
|
28
28
|
export declare function hasRecentDuplicateTask(queue: EvolutionQueueItem[], source: string, preview: string, now: number, reason?: string): boolean;
|
|
29
29
|
export declare function hasEquivalentPromotedRule(dictionary: {
|
|
30
30
|
getAllRules(): Record<string, {
|
|
@@ -34,8 +34,12 @@ export declare function hasEquivalentPromotedRule(dictionary: {
|
|
|
34
34
|
status: string;
|
|
35
35
|
}>;
|
|
36
36
|
}, phrase: string): boolean;
|
|
37
|
-
export declare function trackPainCandidate(text: string, wctx: WorkspaceContext): void
|
|
38
|
-
export declare function processPromotion(wctx: WorkspaceContext, logger: any, eventLog: any): void
|
|
37
|
+
export declare function trackPainCandidate(text: string, wctx: WorkspaceContext): Promise<void>;
|
|
38
|
+
export declare function processPromotion(wctx: WorkspaceContext, logger: any, eventLog: any): Promise<void>;
|
|
39
|
+
export declare function registerEvolutionTaskSession(workspaceResolve: (key: string) => string, taskId: string, sessionKey: string, logger?: {
|
|
40
|
+
warn?: (message: string) => void;
|
|
41
|
+
info?: (message: string) => void;
|
|
42
|
+
}): Promise<boolean>;
|
|
39
43
|
export interface ExtendedEvolutionWorkerService {
|
|
40
44
|
id: string;
|
|
41
45
|
api: OpenClawPluginApi | null;
|