principles-disciple 1.7.8 → 1.8.1
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/core/config.d.ts +2 -0
- package/dist/core/session-tracker.d.ts +3 -1
- package/dist/core/session-tracker.js +12 -3
- package/dist/hooks/llm.d.ts +1 -0
- package/dist/hooks/llm.js +17 -70
- package/dist/hooks/progressive-trust-gate.d.ts +1 -0
- package/dist/hooks/progressive-trust-gate.js +45 -0
- package/dist/hooks/prompt.d.ts +2 -0
- package/dist/hooks/prompt.js +27 -6
- package/dist/hooks/subagent.js +2 -2
- package/dist/http/principles-console-route.js +114 -0
- package/dist/service/empathy-observer-manager.d.ts +46 -10
- package/dist/service/empathy-observer-manager.js +249 -64
- package/dist/service/evolution-worker.js +1 -0
- package/dist/service/health-query-service.d.ts +170 -0
- package/dist/service/health-query-service.js +662 -0
- package/dist/service/nocturnal-runtime.d.ts +2 -2
- package/dist/service/nocturnal-runtime.js +75 -4
- package/dist/service/nocturnal-service.js +2 -2
- package/dist/service/subagent-workflow/empathy-observer-workflow-manager.d.ts +48 -0
- package/dist/service/subagent-workflow/empathy-observer-workflow-manager.js +480 -0
- package/dist/service/subagent-workflow/index.d.ts +4 -0
- package/dist/service/subagent-workflow/index.js +3 -0
- package/dist/service/subagent-workflow/runtime-direct-driver.d.ts +77 -0
- package/dist/service/subagent-workflow/runtime-direct-driver.js +75 -0
- package/dist/service/subagent-workflow/types.d.ts +259 -0
- package/dist/service/subagent-workflow/types.js +11 -0
- package/dist/service/subagent-workflow/workflow-store.d.ts +26 -0
- package/dist/service/subagent-workflow/workflow-store.js +165 -0
- package/dist/tools/deep-reflect.js +2 -2
- package/openclaw.plugin.json +6 -1
- package/package.json +3 -3
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
2
2
|
import { trackFriction } from '../core/session-tracker.js';
|
|
3
3
|
import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
|
|
4
|
-
const OBSERVER_SESSION_PREFIX = '
|
|
4
|
+
const OBSERVER_SESSION_PREFIX = 'agent:main:subagent:empathy-obs-';
|
|
5
|
+
// Default timeout for waitForRun (30 seconds)
|
|
6
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
5
7
|
export class EmpathyObserverManager {
|
|
6
8
|
static instance;
|
|
7
9
|
sessionLocks = new Map();
|
|
10
|
+
activeRuns = new Map();
|
|
11
|
+
completedSessions = new Map();
|
|
8
12
|
constructor() { }
|
|
9
13
|
static getInstance() {
|
|
10
14
|
if (!EmpathyObserverManager.instance) {
|
|
@@ -13,105 +17,236 @@ export class EmpathyObserverManager {
|
|
|
13
17
|
return EmpathyObserverManager.instance;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* every method throws "only available during a gateway request".
|
|
19
|
-
* We cache the result to avoid repeated probing.
|
|
20
|
+
* Build a safe session key for empathy observer
|
|
21
|
+
* Format: agent:main:subagent:empathy-obs-{safeParentSessionId}-{timestamp}
|
|
20
22
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
buildEmpathyObserverSessionKey(parentSessionId) {
|
|
24
|
+
const safeParentSessionId = parentSessionId
|
|
25
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
26
|
+
.substring(0, 64);
|
|
27
|
+
const timestamp = Date.now();
|
|
28
|
+
return `${OBSERVER_SESSION_PREFIX}${safeParentSessionId}-${timestamp}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if a session key is an empathy observer session
|
|
32
|
+
*/
|
|
33
|
+
isObserverSession(sessionKey) {
|
|
34
|
+
return isEmpathyObserverSession(sessionKey);
|
|
35
|
+
}
|
|
36
|
+
markCompleted(observerSessionKey) {
|
|
37
|
+
this.completedSessions.set(observerSessionKey, Date.now());
|
|
38
|
+
}
|
|
39
|
+
isCompleted(observerSessionKey) {
|
|
40
|
+
const timestamp = this.completedSessions.get(observerSessionKey);
|
|
41
|
+
if (!timestamp)
|
|
42
|
+
return false;
|
|
43
|
+
if (Date.now() - timestamp > 5 * 60 * 1000) {
|
|
44
|
+
this.completedSessions.delete(observerSessionKey);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
isActive(parentSessionId) {
|
|
50
|
+
const metadata = this.activeRuns.get(parentSessionId);
|
|
51
|
+
if (!metadata)
|
|
52
|
+
return false;
|
|
53
|
+
if (metadata.timedOutAt || metadata.erroredAt) {
|
|
54
|
+
if (!metadata.observedAt)
|
|
41
55
|
return true;
|
|
56
|
+
// TTL-based cleanup of orphaned timed-out/error entries:
|
|
57
|
+
// if fallback never fired, expire the block so parent session recovers
|
|
58
|
+
if (Date.now() - metadata.observedAt > 5 * 60 * 1000) {
|
|
59
|
+
this.activeRuns.delete(parentSessionId);
|
|
60
|
+
if (this.sessionLocks.get(parentSessionId) === metadata.observerSessionKey) {
|
|
61
|
+
this.sessionLocks.delete(parentSessionId);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
42
64
|
}
|
|
43
|
-
|
|
44
|
-
return false;
|
|
65
|
+
return true;
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
67
|
+
if (Date.now() - metadata.startedAt > 5 * 60 * 1000) {
|
|
68
|
+
this.activeRuns.delete(parentSessionId);
|
|
69
|
+
if (this.sessionLocks.get(parentSessionId) === metadata.observerSessionKey) {
|
|
70
|
+
this.sessionLocks.delete(parentSessionId);
|
|
71
|
+
}
|
|
49
72
|
return false;
|
|
50
73
|
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
getActiveMetadata(parentSessionId) {
|
|
77
|
+
return this.activeRuns.get(parentSessionId);
|
|
51
78
|
}
|
|
52
79
|
shouldTrigger(api, sessionId) {
|
|
53
|
-
if (!api || !sessionId)
|
|
80
|
+
if (!api || !sessionId) {
|
|
81
|
+
api?.logger?.warn?.('[PD:EmpathyObserver] shouldTrigger=false: api or sessionId null');
|
|
54
82
|
return false;
|
|
83
|
+
}
|
|
55
84
|
const enabled = api.config?.empathy_engine?.enabled !== false;
|
|
56
|
-
if (!enabled)
|
|
85
|
+
if (!enabled) {
|
|
86
|
+
api.logger?.warn?.('[PD:EmpathyObserver] shouldTrigger=false: empathy_engine disabled');
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
// Skip BOOT sessions - they run outside "gateway request" context where subagent.run() is unavailable
|
|
90
|
+
if (sessionId.startsWith('boot-')) {
|
|
91
|
+
api.logger?.warn?.('[PD:EmpathyObserver] shouldTrigger=false: boot session (gateway request context unavailable)');
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const subagentOk = isSubagentRuntimeAvailable(api.runtime?.subagent);
|
|
95
|
+
if (!subagentOk) {
|
|
96
|
+
api.logger?.warn?.('[PD:EmpathyObserver] shouldTrigger=false: subagent runtime unavailable');
|
|
57
97
|
return false;
|
|
58
|
-
|
|
98
|
+
}
|
|
99
|
+
if (this.isActive(sessionId)) {
|
|
100
|
+
api.logger?.warn?.(`[PD:EmpathyObserver] shouldTrigger=false: session ${sessionId} has active run`);
|
|
59
101
|
return false;
|
|
60
|
-
|
|
102
|
+
}
|
|
103
|
+
api.logger?.info?.(`[PD:EmpathyObserver] shouldTrigger=true for session ${sessionId}`);
|
|
104
|
+
return true;
|
|
61
105
|
}
|
|
62
|
-
async spawn(api, sessionId, userMessage) {
|
|
106
|
+
async spawn(api, sessionId, userMessage, workspaceDir) {
|
|
63
107
|
if (!api)
|
|
64
108
|
return null;
|
|
65
|
-
if (!this.shouldTrigger(api, sessionId))
|
|
109
|
+
if (!this.shouldTrigger(api, sessionId)) {
|
|
110
|
+
api?.logger?.warn?.(`[PD:EmpathyObserver] spawn skipped: shouldTrigger=false for session ${sessionId}`);
|
|
66
111
|
return null;
|
|
67
|
-
|
|
112
|
+
}
|
|
113
|
+
if (!userMessage?.trim()) {
|
|
114
|
+
api?.logger?.warn?.('[PD:EmpathyObserver] spawn skipped: empty userMessage');
|
|
68
115
|
return null;
|
|
69
|
-
|
|
70
|
-
const sessionKey =
|
|
116
|
+
}
|
|
117
|
+
const sessionKey = this.buildEmpathyObserverSessionKey(sessionId);
|
|
71
118
|
this.sessionLocks.set(sessionId, sessionKey);
|
|
119
|
+
api.logger.info(`[PD:EmpathyObserver] Spawn starting for session ${sessionId}, key=${sessionKey}`);
|
|
72
120
|
const prompt = [
|
|
73
121
|
'You are an empathy observer.',
|
|
74
122
|
'Analyze ONLY the user message and return strict JSON (no markdown):',
|
|
75
123
|
'{"damageDetected": boolean, "severity": "mild|moderate|severe", "confidence": number, "reason": string}',
|
|
76
124
|
`User message: ${JSON.stringify(userMessage.trim())}`,
|
|
77
125
|
].join('\n');
|
|
126
|
+
let runId;
|
|
78
127
|
try {
|
|
79
|
-
await api.runtime.subagent.run({
|
|
128
|
+
const result = await api.runtime.subagent.run({
|
|
80
129
|
sessionKey,
|
|
81
130
|
message: prompt,
|
|
82
131
|
lane: 'subagent',
|
|
83
132
|
deliver: false,
|
|
84
|
-
idempotencyKey: `${sessionId}:${
|
|
133
|
+
idempotencyKey: `${sessionId}:${Date.now()}`,
|
|
134
|
+
expectsCompletionMessage: true,
|
|
85
135
|
});
|
|
86
|
-
|
|
87
|
-
|
|
136
|
+
runId = result.runId;
|
|
137
|
+
api.logger.info(`[PD:EmpathyObserver] Spawn succeeded for ${sessionKey}, runId=${runId}`);
|
|
88
138
|
}
|
|
89
139
|
catch (error) {
|
|
90
140
|
this.sessionLocks.delete(sessionId);
|
|
91
|
-
api.logger.warn(`[PD:EmpathyObserver]
|
|
141
|
+
api.logger.warn(`[PD:EmpathyObserver] Spawn failed for ${sessionId}: ${String(error)}`);
|
|
92
142
|
return null;
|
|
93
143
|
}
|
|
144
|
+
this.activeRuns.set(sessionId, {
|
|
145
|
+
runId,
|
|
146
|
+
parentSessionId: sessionId,
|
|
147
|
+
observerSessionKey: sessionKey,
|
|
148
|
+
workspaceDir,
|
|
149
|
+
startedAt: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
this.finalizeRun(api, sessionId, sessionKey, workspaceDir).catch(async (err) => {
|
|
152
|
+
api.logger.warn(`[PD:EmpathyObserver] finalizeRun failed (will retry once): ${String(err)}`);
|
|
153
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
154
|
+
try {
|
|
155
|
+
await this.finalizeRun(api, sessionId, sessionKey, workspaceDir);
|
|
156
|
+
}
|
|
157
|
+
catch (retryErr) {
|
|
158
|
+
api.logger.warn(`[PD:EmpathyObserver] finalizeRun retry also failed: ${String(retryErr)}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return sessionKey;
|
|
94
162
|
}
|
|
95
|
-
|
|
96
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Main回收链路: 使用 waitForRun 驱动回收
|
|
165
|
+
* 仅在 ok 时回收; timeout/exception 保留 session 由 fallback 处理
|
|
166
|
+
*/
|
|
167
|
+
async finalizeRun(api, parentSessionId, observerSessionKey, workspaceDir) {
|
|
168
|
+
const metadata = this.activeRuns.get(parentSessionId);
|
|
169
|
+
const runId = metadata?.runId;
|
|
170
|
+
if (!runId) {
|
|
171
|
+
api.logger.warn(`[PD:EmpathyObserver] finalizeRun: no runId for ${observerSessionKey}`);
|
|
172
|
+
this.cleanupState(parentSessionId, observerSessionKey);
|
|
97
173
|
return;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
174
|
+
}
|
|
175
|
+
api.logger.info(`[PD:EmpathyObserver] finalizeRun: waiting for runId=${runId}`);
|
|
176
|
+
let waitResult;
|
|
177
|
+
try {
|
|
178
|
+
waitResult = await api.runtime.subagent.waitForRun({
|
|
179
|
+
runId,
|
|
180
|
+
timeoutMs: DEFAULT_WAIT_TIMEOUT_MS,
|
|
181
|
+
});
|
|
182
|
+
api.logger.info(`[PD:EmpathyObserver] finalizeRun: wait completed status=${waitResult.status}`);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
api.logger.warn(`[PD:EmpathyObserver] finalizeRun: waitForRun threw for ${runId}: ${String(error)}`);
|
|
186
|
+
const updatedMetadata = this.activeRuns.get(parentSessionId);
|
|
187
|
+
if (updatedMetadata) {
|
|
188
|
+
updatedMetadata.erroredAt = Date.now();
|
|
189
|
+
updatedMetadata.observedAt = Date.now();
|
|
190
|
+
}
|
|
191
|
+
this.cleanupState(parentSessionId, observerSessionKey, false);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (waitResult.status === 'timeout') {
|
|
195
|
+
api.logger.warn(`[PD:EmpathyObserver] finalizeRun: observer wait timed out; deferring cleanup for ${observerSessionKey}`);
|
|
196
|
+
const updatedMetadata = this.activeRuns.get(parentSessionId);
|
|
197
|
+
if (updatedMetadata) {
|
|
198
|
+
updatedMetadata.timedOutAt = Date.now();
|
|
199
|
+
updatedMetadata.observedAt = Date.now();
|
|
102
200
|
}
|
|
103
|
-
|
|
201
|
+
this.cleanupState(parentSessionId, observerSessionKey, false);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (waitResult.status === 'error') {
|
|
205
|
+
api.logger.warn(`[PD:EmpathyObserver] finalizeRun: observer run errored: ${waitResult.error}; deferring cleanup for ${observerSessionKey}`);
|
|
206
|
+
const updatedMetadata = this.activeRuns.get(parentSessionId);
|
|
207
|
+
if (updatedMetadata) {
|
|
208
|
+
updatedMetadata.erroredAt = Date.now();
|
|
209
|
+
updatedMetadata.observedAt = Date.now();
|
|
210
|
+
}
|
|
211
|
+
this.cleanupState(parentSessionId, observerSessionKey, false);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Set observedAt before reapBySession so TTL cleanup works even if reapBySession fails
|
|
215
|
+
const updatedMetadata = this.activeRuns.get(parentSessionId);
|
|
216
|
+
if (updatedMetadata) {
|
|
217
|
+
updatedMetadata.observedAt = Date.now();
|
|
218
|
+
}
|
|
219
|
+
await this.reapBySession(api, observerSessionKey, parentSessionId, workspaceDir);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 统一回收入口: reap + deleteSession + 清理状态
|
|
223
|
+
*/
|
|
224
|
+
async reapBySession(api, observerSessionKey, parentSessionId, workspaceDir) {
|
|
225
|
+
if (this.isCompleted(observerSessionKey)) {
|
|
226
|
+
api.logger.info(`[PD:EmpathyObserver] reapBySession: already processed ${observerSessionKey}, skipping`);
|
|
227
|
+
this.cleanupState(parentSessionId, observerSessionKey);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
api.logger.info(`[PD:EmpathyObserver] reapBySession starting for ${observerSessionKey}`);
|
|
231
|
+
const storedMetadata = this.activeRuns.get(parentSessionId);
|
|
232
|
+
// Use original parentSessionId from metadata, NOT extracted from session key
|
|
233
|
+
// (session key may have been sanitized/truncated and doesn't match original)
|
|
234
|
+
const sessionId = storedMetadata?.parentSessionId || this.extractParentSessionId(observerSessionKey) || parentSessionId;
|
|
235
|
+
let finalized = false;
|
|
104
236
|
try {
|
|
105
237
|
const messages = await api.runtime.subagent.getSessionMessages({
|
|
106
|
-
sessionKey:
|
|
238
|
+
sessionKey: observerSessionKey,
|
|
107
239
|
limit: 20,
|
|
108
240
|
});
|
|
241
|
+
api.logger.info(`[PD:EmpathyObserver] Retrieved messages for ${observerSessionKey}`);
|
|
109
242
|
const rawText = this.extractAssistantText(messages.messages, messages.assistantTexts);
|
|
243
|
+
api.logger?.debug?.(`[PD:EmpathyObserver] Raw observer output for ${observerSessionKey}: ${JSON.stringify(rawText)}`);
|
|
110
244
|
const parsed = this.parseJsonPayload(rawText, api.logger);
|
|
245
|
+
api.logger.info(`[PD:EmpathyObserver] Payload parsed: ${JSON.stringify(parsed)}`);
|
|
111
246
|
if (parsed?.damageDetected && sessionId) {
|
|
112
|
-
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
247
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir: workspaceDir || '' });
|
|
113
248
|
const score = this.scoreFromSeverity(parsed.severity, wctx.config);
|
|
114
|
-
trackFriction(sessionId, score, `observer_empathy_${parsed.severity || 'mild'}`, workspaceDir, { source: 'user_empathy' });
|
|
249
|
+
trackFriction(sessionId, score, `observer_empathy_${parsed.severity || 'mild'}`, workspaceDir || '', { source: 'user_empathy' });
|
|
115
250
|
const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
116
251
|
wctx.eventLog.recordPainSignal(sessionId, {
|
|
117
252
|
score,
|
|
@@ -144,25 +279,72 @@ export class EmpathyObserverManager {
|
|
|
144
279
|
}
|
|
145
280
|
api.logger.info(`[PD:EmpathyObserver] Applied GFI +${score} for ${sessionId}`);
|
|
146
281
|
}
|
|
282
|
+
else {
|
|
283
|
+
api.logger.info(`[PD:EmpathyObserver] No damage detected or no sessionId for ${observerSessionKey}`);
|
|
284
|
+
}
|
|
285
|
+
finalized = true;
|
|
147
286
|
}
|
|
148
287
|
catch (error) {
|
|
149
|
-
api.logger.warn(`[PD:EmpathyObserver]
|
|
288
|
+
api.logger.warn(`[PD:EmpathyObserver] reapBySession failed to read messages for ${observerSessionKey}: ${String(error)}`);
|
|
289
|
+
}
|
|
290
|
+
// Only delete the session if we successfully read messages (finalized=true).
|
|
291
|
+
// When finalized=false (getSessionMessages failed), preserve the session so
|
|
292
|
+
// the subagent_ended fallback can retry or the TTL expiry can unblock the parent.
|
|
293
|
+
if (finalized) {
|
|
294
|
+
try {
|
|
295
|
+
await api.runtime.subagent.deleteSession({ sessionKey: observerSessionKey });
|
|
296
|
+
api.logger.info(`[PD:EmpathyObserver] deleteSession succeeded for ${observerSessionKey}`);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
api.logger.warn(`[PD:EmpathyObserver] deleteSession failed for ${observerSessionKey}: ${String(error)}`);
|
|
300
|
+
}
|
|
301
|
+
this.markCompleted(observerSessionKey);
|
|
150
302
|
}
|
|
151
|
-
|
|
152
|
-
|
|
303
|
+
// Only delete from activeRuns if finalized; otherwise preserve entry for subagent_ended fallback
|
|
304
|
+
this.cleanupState(parentSessionId, observerSessionKey, finalized);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Fallback回收: 由 subagent_ended 触发
|
|
308
|
+
* 仅在主链路未处理时执行补救回收
|
|
309
|
+
*/
|
|
310
|
+
async reap(api, targetSessionKey, workspaceDir) {
|
|
311
|
+
if (!api || !this.isObserverSession(targetSessionKey))
|
|
312
|
+
return;
|
|
313
|
+
if (this.isCompleted(targetSessionKey)) {
|
|
314
|
+
api.logger.info(`[PD:EmpathyObserver] reap fallback: already processed ${targetSessionKey}, skipping`);
|
|
315
|
+
return;
|
|
153
316
|
}
|
|
317
|
+
// Find the original parentSessionId by matching observerSessionKey in activeRuns
|
|
318
|
+
// (the session key may have been sanitized, so we can't rely on extraction alone)
|
|
319
|
+
let parentSessionId = '';
|
|
320
|
+
for (const [key, metadata] of this.activeRuns) {
|
|
321
|
+
if (metadata.observerSessionKey === targetSessionKey) {
|
|
322
|
+
parentSessionId = key;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Fallback: extract from session key (may be truncated but better than nothing for logging)
|
|
327
|
+
if (!parentSessionId) {
|
|
328
|
+
parentSessionId = this.extractParentSessionId(targetSessionKey) || '';
|
|
329
|
+
}
|
|
330
|
+
await this.reapBySession(api, targetSessionKey, parentSessionId, workspaceDir);
|
|
154
331
|
}
|
|
155
|
-
|
|
156
|
-
|
|
332
|
+
cleanupState(parentSessionId, observerSessionKey, deleteFromActiveRuns = true) {
|
|
333
|
+
if (deleteFromActiveRuns) {
|
|
334
|
+
this.activeRuns.delete(parentSessionId);
|
|
335
|
+
}
|
|
336
|
+
if (this.sessionLocks.get(parentSessionId) === observerSessionKey) {
|
|
337
|
+
this.sessionLocks.delete(parentSessionId);
|
|
338
|
+
}
|
|
157
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Extract parent session ID from observer session key
|
|
342
|
+
*/
|
|
158
343
|
extractParentSessionId(sessionKey) {
|
|
159
344
|
if (!this.isObserverSession(sessionKey))
|
|
160
345
|
return null;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
if (marker <= 0)
|
|
164
|
-
return null;
|
|
165
|
-
return rest.slice(0, marker);
|
|
346
|
+
const match = sessionKey.match(/empathy-obs-(.+)-(\d+)$/);
|
|
347
|
+
return match ? match[1] : null;
|
|
166
348
|
}
|
|
167
349
|
parseJsonPayload(rawText, logger) {
|
|
168
350
|
if (!rawText?.trim())
|
|
@@ -227,3 +409,6 @@ export class EmpathyObserverManager {
|
|
|
227
409
|
}
|
|
228
410
|
}
|
|
229
411
|
export const empathyObserverManager = EmpathyObserverManager.getInstance();
|
|
412
|
+
export function isEmpathyObserverSession(sessionKey) {
|
|
413
|
+
return typeof sessionKey === 'string' && sessionKey.startsWith(OBSERVER_SESSION_PREFIX);
|
|
414
|
+
}
|
|
@@ -922,6 +922,7 @@ export const EvolutionWorkerService = {
|
|
|
922
922
|
// This makes nocturnal-runtime a visible part of the worker lifecycle.
|
|
923
923
|
// Phase 2.4: Enqueue sleep_reflection when workspace is idle and not in cooldown.
|
|
924
924
|
const idleResult = checkWorkspaceIdle(wctx.workspaceDir, {});
|
|
925
|
+
logger?.info?.(`[PD:EvolutionWorker] HEARTBEAT cycle=${new Date().toISOString()} idle=${idleResult.isIdle} idleForMs=${idleResult.idleForMs} userActiveSessions=${idleResult.userActiveSessions} abandonedSessions=${idleResult.abandonedSessionIds.length} lastActivityEpoch=${idleResult.mostRecentActivityAt}`);
|
|
925
926
|
if (idleResult.isIdle) {
|
|
926
927
|
logger?.debug?.(`[PD:EvolutionWorker] Workspace idle (${idleResult.idleForMs}ms since last activity)`);
|
|
927
928
|
// Phase 2.4: Enqueue sleep_reflection task if not in global cooldown
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
type HealthStage = 'healthy' | 'warning' | 'critical';
|
|
2
|
+
export declare class HealthQueryService {
|
|
3
|
+
private readonly workspaceDir;
|
|
4
|
+
private readonly stateDir;
|
|
5
|
+
private readonly trajectory;
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly eventLog;
|
|
8
|
+
private readonly evolutionReducer;
|
|
9
|
+
private readonly uiDb;
|
|
10
|
+
private readonly tableColumnCache;
|
|
11
|
+
constructor(workspaceDir: string);
|
|
12
|
+
dispose(): void;
|
|
13
|
+
getOverviewHealth(): {
|
|
14
|
+
gfi: {
|
|
15
|
+
current: number;
|
|
16
|
+
peakToday: number;
|
|
17
|
+
threshold: number;
|
|
18
|
+
};
|
|
19
|
+
trust: {
|
|
20
|
+
stage: number;
|
|
21
|
+
stageLabel: string;
|
|
22
|
+
score: number;
|
|
23
|
+
};
|
|
24
|
+
evolution: {
|
|
25
|
+
tier: string;
|
|
26
|
+
points: number;
|
|
27
|
+
};
|
|
28
|
+
painFlag: {
|
|
29
|
+
active: boolean;
|
|
30
|
+
source: string | null;
|
|
31
|
+
score: number | null;
|
|
32
|
+
};
|
|
33
|
+
principles: {
|
|
34
|
+
candidate: number;
|
|
35
|
+
probation: number;
|
|
36
|
+
active: number;
|
|
37
|
+
deprecated: number;
|
|
38
|
+
};
|
|
39
|
+
queue: {
|
|
40
|
+
pending: number;
|
|
41
|
+
inProgress: number;
|
|
42
|
+
completed: number;
|
|
43
|
+
};
|
|
44
|
+
activeStage: HealthStage;
|
|
45
|
+
};
|
|
46
|
+
getEvolutionPrinciples(): {
|
|
47
|
+
principles: {
|
|
48
|
+
summary: {
|
|
49
|
+
candidate: number;
|
|
50
|
+
probation: number;
|
|
51
|
+
active: number;
|
|
52
|
+
deprecated: number;
|
|
53
|
+
};
|
|
54
|
+
recent: Array<{
|
|
55
|
+
principleId: string;
|
|
56
|
+
status: string;
|
|
57
|
+
triggerPattern: string;
|
|
58
|
+
action: string;
|
|
59
|
+
fromStatus: string;
|
|
60
|
+
toStatus: string;
|
|
61
|
+
timestamp: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
nocturnalTraining: {
|
|
65
|
+
queue: {
|
|
66
|
+
pending: number;
|
|
67
|
+
inProgress: number;
|
|
68
|
+
completed: number;
|
|
69
|
+
};
|
|
70
|
+
trinityRecords: Array<{
|
|
71
|
+
artifactId: string;
|
|
72
|
+
status: string;
|
|
73
|
+
createdAt: string;
|
|
74
|
+
}>;
|
|
75
|
+
arbiterPassRate: number;
|
|
76
|
+
orpoSampleCount: number;
|
|
77
|
+
deployments: Array<{
|
|
78
|
+
modelId: string;
|
|
79
|
+
status: string;
|
|
80
|
+
checkpointPath: string | null;
|
|
81
|
+
}>;
|
|
82
|
+
};
|
|
83
|
+
painSourceDistribution: Record<string, number>;
|
|
84
|
+
activeStage: string;
|
|
85
|
+
};
|
|
86
|
+
getFeedbackGfi(): {
|
|
87
|
+
current: number;
|
|
88
|
+
peakToday: number;
|
|
89
|
+
threshold: number;
|
|
90
|
+
trend: Array<{
|
|
91
|
+
hour: string;
|
|
92
|
+
value: number;
|
|
93
|
+
}>;
|
|
94
|
+
sources: Record<string, number>;
|
|
95
|
+
};
|
|
96
|
+
getFeedbackEmpathyEvents(limit?: number): Array<{
|
|
97
|
+
timestamp: string;
|
|
98
|
+
severity: string;
|
|
99
|
+
score: number;
|
|
100
|
+
reason: string;
|
|
101
|
+
origin: string;
|
|
102
|
+
gfiAfter: number;
|
|
103
|
+
}>;
|
|
104
|
+
getFeedbackGateBlocks(limit?: number): Array<{
|
|
105
|
+
timestamp: string;
|
|
106
|
+
toolName: string;
|
|
107
|
+
reason: string;
|
|
108
|
+
gfi: number;
|
|
109
|
+
trustStage: number;
|
|
110
|
+
}>;
|
|
111
|
+
getGateStats(): {
|
|
112
|
+
today: {
|
|
113
|
+
gfiBlocks: number;
|
|
114
|
+
stageBlocks: number;
|
|
115
|
+
p03Blocks: number;
|
|
116
|
+
bypassAttempts: number;
|
|
117
|
+
p16Exemptions: number;
|
|
118
|
+
};
|
|
119
|
+
trust: {
|
|
120
|
+
stage: number;
|
|
121
|
+
score: number;
|
|
122
|
+
status: string;
|
|
123
|
+
};
|
|
124
|
+
evolution: {
|
|
125
|
+
tier: string;
|
|
126
|
+
points: number;
|
|
127
|
+
status: string;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
getGateBlocks(limit?: number): Array<{
|
|
131
|
+
timestamp: string;
|
|
132
|
+
toolName: string;
|
|
133
|
+
filePath: string | null;
|
|
134
|
+
reason: string;
|
|
135
|
+
gateType: string;
|
|
136
|
+
gfi: number;
|
|
137
|
+
trustStage: number;
|
|
138
|
+
}>;
|
|
139
|
+
private readTrust;
|
|
140
|
+
private readEvolutionScore;
|
|
141
|
+
private readQueueStats;
|
|
142
|
+
private readPainFlag;
|
|
143
|
+
private getCurrentSession;
|
|
144
|
+
private getGfiThreshold;
|
|
145
|
+
private computeHealthStage;
|
|
146
|
+
private getTrustStageLabel;
|
|
147
|
+
private inferTrustStageFromScore;
|
|
148
|
+
private normalizeTierName;
|
|
149
|
+
private readRecentPrincipleChanges;
|
|
150
|
+
private mapPrincipleEvent;
|
|
151
|
+
private readNocturnalTraining;
|
|
152
|
+
private readPainSourceDistribution;
|
|
153
|
+
private readEvolutionActiveStage;
|
|
154
|
+
private readMergedEvents;
|
|
155
|
+
private readPersistedEvents;
|
|
156
|
+
private getBufferedEvents;
|
|
157
|
+
private getEventDedupKey;
|
|
158
|
+
private readGateBlocksRaw;
|
|
159
|
+
private resolveGateBlockGfi;
|
|
160
|
+
private resolveGateBlockTrustStage;
|
|
161
|
+
private resolveGateType;
|
|
162
|
+
private hasTableColumn;
|
|
163
|
+
private scoreToStatus;
|
|
164
|
+
private evolutionToStatus;
|
|
165
|
+
private safeListFiles;
|
|
166
|
+
private readJsonFile;
|
|
167
|
+
private asNumber;
|
|
168
|
+
private asNullableNumber;
|
|
169
|
+
}
|
|
170
|
+
export {};
|