@esotech/contextuate 2.0.0 → 2.1.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/README.md +169 -1
- package/dist/commands/claude.d.ts +21 -0
- package/dist/commands/claude.js +213 -0
- package/dist/commands/context.d.ts +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +67 -6
- package/dist/commands/install.d.ts +28 -0
- package/dist/commands/install.js +100 -11
- package/dist/commands/monitor.d.ts +55 -0
- package/dist/commands/monitor.js +1007 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +113 -1
- package/dist/monitor/daemon/circuit-breaker.d.ts +121 -0
- package/dist/monitor/daemon/circuit-breaker.js +552 -0
- package/dist/monitor/daemon/cli.d.ts +8 -0
- package/dist/monitor/daemon/cli.js +82 -0
- package/dist/monitor/daemon/index.d.ts +137 -0
- package/dist/monitor/daemon/index.js +695 -0
- package/dist/monitor/daemon/notifier.d.ts +25 -0
- package/dist/monitor/daemon/notifier.js +98 -0
- package/dist/monitor/daemon/processor.d.ts +89 -0
- package/dist/monitor/daemon/processor.js +455 -0
- package/dist/monitor/daemon/state.d.ts +80 -0
- package/dist/monitor/daemon/state.js +162 -0
- package/dist/monitor/daemon/watcher.d.ts +47 -0
- package/dist/monitor/daemon/watcher.js +171 -0
- package/dist/monitor/daemon/wrapper-manager.d.ts +106 -0
- package/dist/monitor/daemon/wrapper-manager.js +374 -0
- package/dist/monitor/hooks/emit-event.js +652 -0
- package/dist/monitor/persistence/file-store.d.ts +88 -0
- package/dist/monitor/persistence/file-store.js +335 -0
- package/dist/monitor/persistence/index.d.ts +7 -0
- package/dist/monitor/persistence/index.js +10 -0
- package/dist/monitor/server/adapters/redis.d.ts +38 -0
- package/dist/monitor/server/adapters/redis.js +213 -0
- package/dist/monitor/server/adapters/unix-socket.d.ts +33 -0
- package/dist/monitor/server/adapters/unix-socket.js +182 -0
- package/dist/monitor/server/broker.d.ts +135 -0
- package/dist/monitor/server/broker.js +475 -0
- package/dist/monitor/server/cli.d.ts +8 -0
- package/dist/monitor/server/cli.js +98 -0
- package/dist/monitor/server/fastify.d.ts +16 -0
- package/dist/monitor/server/fastify.js +184 -0
- package/dist/monitor/server/index.d.ts +36 -0
- package/dist/monitor/server/index.js +153 -0
- package/dist/monitor/server/websocket.d.ts +80 -0
- package/dist/monitor/server/websocket.js +453 -0
- package/dist/monitor/ui/assets/index-4IssW9On.js +59 -0
- package/dist/monitor/ui/assets/index-vo9hLe5R.css +32 -0
- package/dist/monitor/ui/favicon.png +0 -0
- package/dist/monitor/ui/index.html +14 -0
- package/dist/monitor/ui/logo.png +0 -0
- package/dist/monitor/ui/logo.svg +1 -0
- package/dist/runtime/driver.d.ts +16 -0
- package/dist/runtime/tools.d.ts +10 -0
- package/dist/templates/README.md +33 -7
- package/dist/templates/agents/aegis.md +4 -0
- package/dist/templates/agents/archon.md +13 -22
- package/dist/templates/agents/atlas.md +4 -0
- package/dist/templates/agents/canvas.md +4 -0
- package/dist/templates/agents/chronicle.md +4 -0
- package/dist/templates/agents/chronos.md +4 -0
- package/dist/templates/agents/cipher.md +4 -0
- package/dist/templates/agents/crucible.md +4 -0
- package/dist/templates/agents/echo.md +4 -0
- package/dist/templates/agents/forge.md +4 -0
- package/dist/templates/agents/ledger.md +4 -0
- package/dist/templates/agents/meridian.md +4 -0
- package/dist/templates/agents/nexus.md +4 -0
- package/dist/templates/agents/pythia.md +217 -0
- package/dist/templates/agents/scribe.md +4 -0
- package/dist/templates/agents/sentinel.md +4 -0
- package/dist/templates/agents/{oracle.md → thoth.md} +11 -7
- package/dist/templates/agents/unity.md +4 -0
- package/dist/templates/agents/vox.md +4 -0
- package/dist/templates/agents/weaver.md +4 -0
- package/dist/templates/commands/consult.md +138 -0
- package/dist/templates/commands/orchestrate.md +173 -0
- package/dist/templates/framework-agents/documentation-expert.md +3 -3
- package/dist/templates/framework-agents/tools-expert.md +8 -8
- package/dist/templates/standards/agent-roles.md +68 -21
- package/dist/templates/standards/coding-standards.md +9 -26
- package/dist/templates/templates/context.md +17 -2
- package/dist/templates/templates/contextuate.md +21 -28
- package/dist/templates/tools/{agent-creator.tool.md → agent-creator.md} +3 -3
- package/dist/types/monitor.d.ts +660 -0
- package/dist/types/monitor.js +75 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/tokens.d.ts +10 -0
- package/package.json +18 -5
- package/dist/templates/version.json +0 -8
- /package/dist/templates/templates/standards/{go.standards.md → go.md} +0 -0
- /package/dist/templates/templates/standards/{java.standards.md → java.md} +0 -0
- /package/dist/templates/templates/standards/{javascript.standards.md → javascript.md} +0 -0
- /package/dist/templates/templates/standards/{php.standards.md → php.md} +0 -0
- /package/dist/templates/templates/standards/{python.standards.md → python.md} +0 -0
- /package/dist/templates/tools/{quickref.tool.md → quickref.md} +0 -0
- /package/dist/templates/tools/{spawn.tool.md → spawn.md} +0 -0
- /package/dist/templates/tools/{standards-detector.tool.md → standards-detector.md} +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Circuit Breaker
|
|
4
|
+
*
|
|
5
|
+
* Monitors Claude Code sessions for stagnation and takes corrective action.
|
|
6
|
+
* Inspired by Michael Nygard's "Release It!" circuit breaker pattern.
|
|
7
|
+
*
|
|
8
|
+
* States:
|
|
9
|
+
* - CLOSED: Normal operation, progress being made
|
|
10
|
+
* - HALF_OPEN: Warning state, monitoring for recovery
|
|
11
|
+
* - OPEN: Stagnation detected, intervention required
|
|
12
|
+
*
|
|
13
|
+
* Detection methods:
|
|
14
|
+
* - Time-based: No events for X seconds, no progress for Y seconds
|
|
15
|
+
* - Loop-based: N loops without file changes, M consecutive errors
|
|
16
|
+
*
|
|
17
|
+
* Actions:
|
|
18
|
+
* - Alert: Notify UI of state change
|
|
19
|
+
* - Inject prompt: Send "you're stuck" message to Claude
|
|
20
|
+
* - Kill: Terminate the session
|
|
21
|
+
* - Restart: Kill and spawn new session
|
|
22
|
+
*/
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.CircuitBreaker = void 0;
|
|
58
|
+
const cron = __importStar(require("node-cron"));
|
|
59
|
+
const monitor_js_1 = require("../../types/monitor.js");
|
|
60
|
+
/**
|
|
61
|
+
* Circuit Breaker class
|
|
62
|
+
*
|
|
63
|
+
* Monitors session health and takes action when stagnation is detected.
|
|
64
|
+
*/
|
|
65
|
+
class CircuitBreaker {
|
|
66
|
+
constructor(config, wrapperManager, onAlert) {
|
|
67
|
+
this.sessionHealth = new Map();
|
|
68
|
+
this.cronJob = null;
|
|
69
|
+
this.pendingKills = new Map();
|
|
70
|
+
this.config = { ...monitor_js_1.DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
|
|
71
|
+
this.wrapperManager = wrapperManager;
|
|
72
|
+
this.onAlert = onAlert;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Start the cron-based health monitoring
|
|
76
|
+
*/
|
|
77
|
+
start() {
|
|
78
|
+
if (!this.config.enabled) {
|
|
79
|
+
console.log('[CircuitBreaker] Disabled by configuration');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(`[CircuitBreaker] Starting health checks: ${this.config.healthCheckInterval}`);
|
|
83
|
+
this.cronJob = cron.schedule(this.config.healthCheckInterval, () => {
|
|
84
|
+
this.runHealthChecks();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Stop health monitoring
|
|
89
|
+
*/
|
|
90
|
+
stop() {
|
|
91
|
+
if (this.cronJob) {
|
|
92
|
+
this.cronJob.stop();
|
|
93
|
+
this.cronJob = null;
|
|
94
|
+
console.log('[CircuitBreaker] Stopped health checks');
|
|
95
|
+
}
|
|
96
|
+
// Clear any pending kill timers
|
|
97
|
+
for (const [sessionId, timeout] of this.pendingKills) {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
}
|
|
100
|
+
this.pendingKills.clear();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Update configuration at runtime
|
|
104
|
+
*/
|
|
105
|
+
updateConfig(config) {
|
|
106
|
+
const wasEnabled = this.config.enabled;
|
|
107
|
+
this.config = { ...this.config, ...config };
|
|
108
|
+
// Handle enable/disable changes
|
|
109
|
+
if (!wasEnabled && this.config.enabled) {
|
|
110
|
+
this.start();
|
|
111
|
+
}
|
|
112
|
+
else if (wasEnabled && !this.config.enabled) {
|
|
113
|
+
this.stop();
|
|
114
|
+
}
|
|
115
|
+
// If cron schedule changed, restart
|
|
116
|
+
if (this.cronJob && config.healthCheckInterval) {
|
|
117
|
+
this.stop();
|
|
118
|
+
this.start();
|
|
119
|
+
}
|
|
120
|
+
console.log('[CircuitBreaker] Configuration updated');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get current configuration
|
|
124
|
+
*/
|
|
125
|
+
getConfig() {
|
|
126
|
+
return { ...this.config };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Scheduled health check - runs on cron interval
|
|
130
|
+
*/
|
|
131
|
+
runHealthChecks() {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
for (const [sessionId, health] of this.sessionHealth) {
|
|
134
|
+
// Skip sessions that are already OPEN and have no wrapper
|
|
135
|
+
if (health.state === 'OPEN' && !health.wrapperId)
|
|
136
|
+
continue;
|
|
137
|
+
// Skip sessions that haven't had any events yet
|
|
138
|
+
if (health.totalEvents === 0)
|
|
139
|
+
continue;
|
|
140
|
+
const timeSinceEvent = (now - health.lastEventTime) / 1000;
|
|
141
|
+
const timeSinceProgress = (now - health.lastProgressTime) / 1000;
|
|
142
|
+
const sessionDuration = (now - health.sessionStartTime) / 1000;
|
|
143
|
+
// Check 1: No events at all (process might be hung)
|
|
144
|
+
if (timeSinceEvent > this.config.noEventTimeout) {
|
|
145
|
+
this.transitionState(health, 'OPEN', 'no_events', {
|
|
146
|
+
message: `No events for ${Math.round(timeSinceEvent)}s`,
|
|
147
|
+
timeSinceEvent,
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Check 2: No progress (events happening but no file changes)
|
|
152
|
+
if (timeSinceProgress > this.config.noProgressTimeout) {
|
|
153
|
+
if (health.state === 'CLOSED') {
|
|
154
|
+
this.transitionState(health, 'HALF_OPEN', 'no_progress', {
|
|
155
|
+
message: `No file changes for ${Math.round(timeSinceProgress)}s`,
|
|
156
|
+
timeSinceProgress,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (health.state === 'HALF_OPEN' &&
|
|
160
|
+
timeSinceProgress > this.config.noProgressTimeout * 1.5) {
|
|
161
|
+
this.transitionState(health, 'OPEN', 'no_progress_extended', {
|
|
162
|
+
message: `Still no progress after warning`,
|
|
163
|
+
timeSinceProgress,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Check 3: Session exceeded max duration
|
|
169
|
+
if (sessionDuration > this.config.maxSessionDuration) {
|
|
170
|
+
this.transitionState(health, 'OPEN', 'max_duration', {
|
|
171
|
+
message: `Session exceeded ${this.config.maxSessionDuration}s limit`,
|
|
172
|
+
sessionDuration,
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Check 4: Recovery detection - if we were warning but now have progress
|
|
177
|
+
if (health.state === 'HALF_OPEN' &&
|
|
178
|
+
timeSinceProgress < this.config.noProgressTimeout / 2) {
|
|
179
|
+
this.transitionState(health, 'CLOSED', 'recovered', {
|
|
180
|
+
message: 'Progress detected, circuit recovered',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Process an incoming event (called by EventProcessor)
|
|
187
|
+
*/
|
|
188
|
+
processEvent(event, wrapperId) {
|
|
189
|
+
if (!this.config.enabled)
|
|
190
|
+
return;
|
|
191
|
+
const health = this.getOrCreateHealth(event.sessionId, wrapperId);
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
// Update timestamps
|
|
194
|
+
health.lastEventTime = now;
|
|
195
|
+
health.totalEvents++;
|
|
196
|
+
// Track errors
|
|
197
|
+
if (event.eventType === 'tool_error') {
|
|
198
|
+
health.consecutiveErrors++;
|
|
199
|
+
health.totalErrors++;
|
|
200
|
+
health.lastError = event.data.error?.message || 'Unknown error';
|
|
201
|
+
// Immediate check for error threshold
|
|
202
|
+
if (health.consecutiveErrors >= this.config.sameErrorThreshold) {
|
|
203
|
+
this.transitionState(health, 'OPEN', 'error_threshold', {
|
|
204
|
+
message: `${health.consecutiveErrors} consecutive errors`,
|
|
205
|
+
lastError: health.lastError,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Reset consecutive errors on successful operation
|
|
211
|
+
health.consecutiveErrors = 0;
|
|
212
|
+
}
|
|
213
|
+
// Track progress (file modifications)
|
|
214
|
+
if (this.isProgressEvent(event)) {
|
|
215
|
+
health.lastProgressTime = now;
|
|
216
|
+
health.loopsSinceProgress = 0;
|
|
217
|
+
health.filesModified++;
|
|
218
|
+
// Cancel any pending kill
|
|
219
|
+
const pendingKill = this.pendingKills.get(event.sessionId);
|
|
220
|
+
if (pendingKill) {
|
|
221
|
+
clearTimeout(pendingKill);
|
|
222
|
+
this.pendingKills.delete(event.sessionId);
|
|
223
|
+
}
|
|
224
|
+
// Auto-recover from HALF_OPEN on progress
|
|
225
|
+
if (health.state === 'HALF_OPEN') {
|
|
226
|
+
this.transitionState(health, 'CLOSED', 'progress_detected', {
|
|
227
|
+
message: 'File modification detected',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Track loop boundaries (Stop events indicate end of a Claude response)
|
|
232
|
+
if (event.hookType === 'Stop') {
|
|
233
|
+
health.loopsSinceProgress++;
|
|
234
|
+
// Loop-based threshold check
|
|
235
|
+
if (health.loopsSinceProgress >= this.config.noProgressLoops) {
|
|
236
|
+
const targetState = health.state === 'CLOSED' ? 'HALF_OPEN' : 'OPEN';
|
|
237
|
+
this.transitionState(health, targetState, 'loop_threshold', {
|
|
238
|
+
message: `${health.loopsSinceProgress} loops without progress`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Update recommendation based on current state
|
|
243
|
+
health.recommendation = this.getRecommendation(health);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Handle state transitions and take action
|
|
247
|
+
*/
|
|
248
|
+
transitionState(health, newState, reason, context) {
|
|
249
|
+
const prevState = health.state;
|
|
250
|
+
if (prevState === newState)
|
|
251
|
+
return;
|
|
252
|
+
console.log(`[CircuitBreaker] ${health.sessionId}: ${prevState} → ${newState} (${reason})`);
|
|
253
|
+
health.state = newState;
|
|
254
|
+
health.recommendation = this.getRecommendation(health);
|
|
255
|
+
// Emit alert for UI
|
|
256
|
+
const alert = {
|
|
257
|
+
sessionId: health.sessionId,
|
|
258
|
+
wrapperId: health.wrapperId,
|
|
259
|
+
previousState: prevState,
|
|
260
|
+
newState,
|
|
261
|
+
reason,
|
|
262
|
+
message: context.message || reason,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
context,
|
|
265
|
+
};
|
|
266
|
+
this.onAlert(alert);
|
|
267
|
+
// Take action on OPEN
|
|
268
|
+
if (newState === 'OPEN') {
|
|
269
|
+
this.takeAction(health, context);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Take intervention action
|
|
274
|
+
*/
|
|
275
|
+
async takeAction(health, context) {
|
|
276
|
+
if (!health.wrapperId) {
|
|
277
|
+
console.log(`[CircuitBreaker] No wrapper for ${health.sessionId}, alert only`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const wrapper = this.wrapperManager.get(health.wrapperId);
|
|
281
|
+
if (!wrapper) {
|
|
282
|
+
console.log(`[CircuitBreaker] Wrapper ${health.wrapperId} not found, alert only`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Build context-aware prompt
|
|
286
|
+
if (this.config.autoInjectPrompt) {
|
|
287
|
+
const prompt = this.buildInterventionPrompt(health, context);
|
|
288
|
+
console.log(`[CircuitBreaker] Injecting prompt to ${health.wrapperId}`);
|
|
289
|
+
this.wrapperManager.writeInput(health.wrapperId, prompt + '\n');
|
|
290
|
+
// Emit intervention alert
|
|
291
|
+
this.onAlert({
|
|
292
|
+
sessionId: health.sessionId,
|
|
293
|
+
wrapperId: health.wrapperId,
|
|
294
|
+
previousState: health.state,
|
|
295
|
+
newState: health.state,
|
|
296
|
+
reason: 'intervention_sent',
|
|
297
|
+
message: 'Intervention prompt injected',
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
context: { prompt },
|
|
300
|
+
});
|
|
301
|
+
// If autoKill is enabled, schedule a kill after grace period
|
|
302
|
+
if (this.config.autoKill) {
|
|
303
|
+
this.scheduleKill(health, wrapper);
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Direct kill without prompt
|
|
308
|
+
if (this.config.autoKill) {
|
|
309
|
+
this.killAndMaybeRestart(health, wrapper);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Schedule a kill after the grace period
|
|
314
|
+
*/
|
|
315
|
+
scheduleKill(health, wrapper) {
|
|
316
|
+
// Cancel any existing pending kill
|
|
317
|
+
const existing = this.pendingKills.get(health.sessionId);
|
|
318
|
+
if (existing) {
|
|
319
|
+
clearTimeout(existing);
|
|
320
|
+
}
|
|
321
|
+
console.log(`[CircuitBreaker] Scheduling kill for ${health.wrapperId} in ${this.config.gracePeriodMs}ms`);
|
|
322
|
+
const timeout = setTimeout(() => {
|
|
323
|
+
this.pendingKills.delete(health.sessionId);
|
|
324
|
+
// Check if still in OPEN state (might have recovered)
|
|
325
|
+
const currentHealth = this.sessionHealth.get(health.sessionId);
|
|
326
|
+
if (currentHealth?.state === 'OPEN') {
|
|
327
|
+
this.killAndMaybeRestart(currentHealth, wrapper);
|
|
328
|
+
}
|
|
329
|
+
}, this.config.gracePeriodMs);
|
|
330
|
+
this.pendingKills.set(health.sessionId, timeout);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Build a context-aware intervention prompt
|
|
334
|
+
*/
|
|
335
|
+
buildInterventionPrompt(health, context) {
|
|
336
|
+
const base = this.config.stuckPrompt;
|
|
337
|
+
const reason = context.reason;
|
|
338
|
+
// Add context-specific guidance
|
|
339
|
+
if (reason === 'error_threshold') {
|
|
340
|
+
return `${base}\n\nYou've encountered the same error ${health.consecutiveErrors} times: "${health.lastError}"\nConsider: checking your assumptions, trying an alternative method, or asking for clarification.`;
|
|
341
|
+
}
|
|
342
|
+
if (reason === 'no_progress' || reason === 'no_progress_extended') {
|
|
343
|
+
const minutes = Math.round(context.timeSinceProgress / 60);
|
|
344
|
+
return `${base}\n\nNo files have been modified in ${minutes} minutes.\nIf you're researching, that's fine. If you're stuck, please describe the blocker.`;
|
|
345
|
+
}
|
|
346
|
+
if (reason === 'loop_threshold') {
|
|
347
|
+
return `${base}\n\nYou've completed ${health.loopsSinceProgress} iterations without making file changes.\nPlease either make progress on the task or explain what's blocking you.`;
|
|
348
|
+
}
|
|
349
|
+
if (reason === 'no_events') {
|
|
350
|
+
return `${base}\n\nThe session appears to have stalled. Please respond to confirm you're still working.`;
|
|
351
|
+
}
|
|
352
|
+
if (reason === 'max_duration') {
|
|
353
|
+
return `${base}\n\nThis session has been running for a long time. Please summarize your progress and consider wrapping up or continuing in a new session.`;
|
|
354
|
+
}
|
|
355
|
+
return base;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Kill wrapper and optionally restart
|
|
359
|
+
*/
|
|
360
|
+
async killAndMaybeRestart(health, wrapper) {
|
|
361
|
+
if (!health.wrapperId)
|
|
362
|
+
return;
|
|
363
|
+
console.log(`[CircuitBreaker] Killing wrapper ${health.wrapperId}`);
|
|
364
|
+
this.wrapperManager.kill(health.wrapperId);
|
|
365
|
+
// Emit kill alert
|
|
366
|
+
this.onAlert({
|
|
367
|
+
sessionId: health.sessionId,
|
|
368
|
+
wrapperId: health.wrapperId,
|
|
369
|
+
previousState: health.state,
|
|
370
|
+
newState: health.state,
|
|
371
|
+
reason: 'session_killed',
|
|
372
|
+
message: 'Session terminated due to stagnation',
|
|
373
|
+
timestamp: Date.now(),
|
|
374
|
+
});
|
|
375
|
+
if (this.config.autoRestart) {
|
|
376
|
+
setTimeout(async () => {
|
|
377
|
+
console.log(`[CircuitBreaker] Restarting wrapper in ${wrapper.cwd}`);
|
|
378
|
+
try {
|
|
379
|
+
const newId = await this.wrapperManager.spawn({
|
|
380
|
+
cwd: wrapper.cwd,
|
|
381
|
+
args: wrapper.args,
|
|
382
|
+
cols: wrapper.cols,
|
|
383
|
+
rows: wrapper.rows,
|
|
384
|
+
});
|
|
385
|
+
// Emit restart alert
|
|
386
|
+
this.onAlert({
|
|
387
|
+
sessionId: health.sessionId,
|
|
388
|
+
wrapperId: newId,
|
|
389
|
+
previousState: health.state,
|
|
390
|
+
newState: 'CLOSED',
|
|
391
|
+
reason: 'session_restarted',
|
|
392
|
+
message: `Session restarted with new wrapper ${newId}`,
|
|
393
|
+
timestamp: Date.now(),
|
|
394
|
+
});
|
|
395
|
+
console.log(`[CircuitBreaker] New wrapper: ${newId}`);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
console.error(`[CircuitBreaker] Failed to restart:`, err);
|
|
399
|
+
}
|
|
400
|
+
}, 2000);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Check if event indicates file modification progress
|
|
405
|
+
*/
|
|
406
|
+
isProgressEvent(event) {
|
|
407
|
+
if (event.eventType !== 'tool_result')
|
|
408
|
+
return false;
|
|
409
|
+
// File modification tools indicate progress
|
|
410
|
+
const progressTools = [
|
|
411
|
+
'Write',
|
|
412
|
+
'Edit',
|
|
413
|
+
'MultiEdit',
|
|
414
|
+
'NotebookEdit',
|
|
415
|
+
'Bash', // Bash can modify files too
|
|
416
|
+
];
|
|
417
|
+
const toolName = event.data.toolName || '';
|
|
418
|
+
// Direct match
|
|
419
|
+
if (progressTools.includes(toolName)) {
|
|
420
|
+
// For Bash, only count as progress if it looks like a write operation
|
|
421
|
+
if (toolName === 'Bash') {
|
|
422
|
+
const command = String(event.data.toolInput || '');
|
|
423
|
+
const writePatterns = [
|
|
424
|
+
/\b(echo|cat|printf)\s+.*>/,
|
|
425
|
+
/\b(cp|mv|rm|mkdir|touch)\b/,
|
|
426
|
+
/\bgit\s+(commit|add|push)\b/,
|
|
427
|
+
/\bnpm\s+(install|update)\b/,
|
|
428
|
+
];
|
|
429
|
+
return writePatterns.some((p) => p.test(command));
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get recommendation based on current health state
|
|
437
|
+
*/
|
|
438
|
+
getRecommendation(health) {
|
|
439
|
+
switch (health.state) {
|
|
440
|
+
case 'CLOSED':
|
|
441
|
+
return 'continue';
|
|
442
|
+
case 'HALF_OPEN':
|
|
443
|
+
return 'warn';
|
|
444
|
+
case 'OPEN':
|
|
445
|
+
return 'intervene';
|
|
446
|
+
default:
|
|
447
|
+
return 'continue';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Get or create health tracking for a session
|
|
452
|
+
*/
|
|
453
|
+
getOrCreateHealth(sessionId, wrapperId) {
|
|
454
|
+
let health = this.sessionHealth.get(sessionId);
|
|
455
|
+
if (!health) {
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
health = {
|
|
458
|
+
sessionId,
|
|
459
|
+
wrapperId,
|
|
460
|
+
state: 'CLOSED',
|
|
461
|
+
lastEventTime: now,
|
|
462
|
+
lastProgressTime: now,
|
|
463
|
+
sessionStartTime: now,
|
|
464
|
+
loopsSinceProgress: 0,
|
|
465
|
+
consecutiveErrors: 0,
|
|
466
|
+
lastError: null,
|
|
467
|
+
totalEvents: 0,
|
|
468
|
+
totalErrors: 0,
|
|
469
|
+
filesModified: 0,
|
|
470
|
+
recommendation: 'continue',
|
|
471
|
+
};
|
|
472
|
+
this.sessionHealth.set(sessionId, health);
|
|
473
|
+
}
|
|
474
|
+
// Update wrapper ID if provided and not set
|
|
475
|
+
if (wrapperId && !health.wrapperId) {
|
|
476
|
+
health.wrapperId = wrapperId;
|
|
477
|
+
}
|
|
478
|
+
return health;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get health status for all sessions (for UI)
|
|
482
|
+
*/
|
|
483
|
+
getAllHealth() {
|
|
484
|
+
return Array.from(this.sessionHealth.values());
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get health for a specific session
|
|
488
|
+
*/
|
|
489
|
+
getSessionHealth(sessionId) {
|
|
490
|
+
return this.sessionHealth.get(sessionId);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Reset circuit for a session (manual intervention)
|
|
494
|
+
*/
|
|
495
|
+
resetCircuit(sessionId) {
|
|
496
|
+
const health = this.sessionHealth.get(sessionId);
|
|
497
|
+
if (health) {
|
|
498
|
+
const now = Date.now();
|
|
499
|
+
const prevState = health.state;
|
|
500
|
+
health.state = 'CLOSED';
|
|
501
|
+
health.lastProgressTime = now;
|
|
502
|
+
health.loopsSinceProgress = 0;
|
|
503
|
+
health.consecutiveErrors = 0;
|
|
504
|
+
health.recommendation = 'continue';
|
|
505
|
+
// Cancel any pending kill
|
|
506
|
+
const pendingKill = this.pendingKills.get(sessionId);
|
|
507
|
+
if (pendingKill) {
|
|
508
|
+
clearTimeout(pendingKill);
|
|
509
|
+
this.pendingKills.delete(sessionId);
|
|
510
|
+
}
|
|
511
|
+
this.onAlert({
|
|
512
|
+
sessionId,
|
|
513
|
+
wrapperId: health.wrapperId,
|
|
514
|
+
previousState: prevState,
|
|
515
|
+
newState: 'CLOSED',
|
|
516
|
+
reason: 'manual_reset',
|
|
517
|
+
message: 'Circuit manually reset by user',
|
|
518
|
+
timestamp: now,
|
|
519
|
+
});
|
|
520
|
+
console.log(`[CircuitBreaker] Circuit reset for session ${sessionId}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Associate a wrapper with a session (called when correlation is detected)
|
|
525
|
+
*/
|
|
526
|
+
associateWrapper(sessionId, wrapperId) {
|
|
527
|
+
const health = this.sessionHealth.get(sessionId);
|
|
528
|
+
if (health && !health.wrapperId) {
|
|
529
|
+
health.wrapperId = wrapperId;
|
|
530
|
+
console.log(`[CircuitBreaker] Associated session ${sessionId} with wrapper ${wrapperId}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Remove health tracking for a session (on session end)
|
|
535
|
+
*/
|
|
536
|
+
removeSession(sessionId) {
|
|
537
|
+
// Cancel any pending kill
|
|
538
|
+
const pendingKill = this.pendingKills.get(sessionId);
|
|
539
|
+
if (pendingKill) {
|
|
540
|
+
clearTimeout(pendingKill);
|
|
541
|
+
this.pendingKills.delete(sessionId);
|
|
542
|
+
}
|
|
543
|
+
this.sessionHealth.delete(sessionId);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Check if circuit breaker is enabled
|
|
547
|
+
*/
|
|
548
|
+
isEnabled() {
|
|
549
|
+
return this.config.enabled;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
exports.CircuitBreaker = CircuitBreaker;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Daemon CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* This is the entry point for running the daemon as a standalone process.
|
|
7
|
+
* Used when starting the daemon in detached mode.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const commander_1 = require("commander");
|
|
45
|
+
const index_js_1 = require("./index.js");
|
|
46
|
+
const program = new commander_1.Command();
|
|
47
|
+
program
|
|
48
|
+
.name('contextuate-daemon')
|
|
49
|
+
.description('Contextuate Monitor Daemon')
|
|
50
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
51
|
+
.parse(process.argv);
|
|
52
|
+
const options = program.opts();
|
|
53
|
+
async function main() {
|
|
54
|
+
if (!options.config) {
|
|
55
|
+
console.error('[Error] Config file path is required (--config)');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
// Load configuration
|
|
59
|
+
let config;
|
|
60
|
+
try {
|
|
61
|
+
const configContent = await fs.promises.readFile(options.config, 'utf-8');
|
|
62
|
+
config = JSON.parse(configContent);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(`[Error] Failed to load config: ${err.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Start daemon
|
|
69
|
+
const daemon = await (0, index_js_1.startDaemon)(config);
|
|
70
|
+
// Handle shutdown signals
|
|
71
|
+
const shutdown = async () => {
|
|
72
|
+
console.log('\n[Info] Shutting down daemon...');
|
|
73
|
+
await daemon.stop();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
};
|
|
76
|
+
process.on('SIGINT', shutdown);
|
|
77
|
+
process.on('SIGTERM', shutdown);
|
|
78
|
+
}
|
|
79
|
+
main().catch((err) => {
|
|
80
|
+
console.error('[Error] Fatal error:', err);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|