mcp-castor 2026.3.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.
- package/README.md +487 -0
- package/bin/castor.js +706 -0
- package/index.js +206 -0
- package/package.json +97 -0
- package/skills/canary-test-staging/SKILL.md +24 -0
- package/skills/evo-mutation-rollback/SKILL.md +29 -0
- package/skills/hypothesis-generation/SKILL.md +26 -0
- package/skills/traceback-condensing/SKILL.md +26 -0
- package/src/castor_runner.js +469 -0
- package/src/config.js +1204 -0
- package/src/env.js +10 -0
- package/src/evo_engine.js +214 -0
- package/src/harness/core/events.js +75 -0
- package/src/harness/core/kernel.js +209 -0
- package/src/harness/evo/evaluator.js +156 -0
- package/src/harness/evo/evo_operator.js +550 -0
- package/src/harness/evo/lineage_dag.js +383 -0
- package/src/harness/evo/trace_repair.js +173 -0
- package/src/harness/evo/watchdog.js +72 -0
- package/src/harness/loop_detector.js +135 -0
- package/src/harness/runner.js +1216 -0
- package/src/harness/services/ast_service.js +1813 -0
- package/src/harness/services/event_logger.js +275 -0
- package/src/harness/services/mcp_bridge.js +408 -0
- package/src/harness/services/provider_vllm.js +728 -0
- package/src/harness/services/sandbox_fs.js +1238 -0
- package/src/harness/services/searxng_lifecycle.js +254 -0
- package/src/harness/services/shell_executor.js +264 -0
- package/src/harness/services/shell_validator.js +506 -0
- package/src/harness/services/web_service.js +828 -0
- package/src/platform.js +344 -0
- package/src/repetition_detector.js +139 -0
- package/src/semaphore.js +373 -0
- package/src/server_lifecycle.js +781 -0
- package/src/skills.js +400 -0
- package/src/state_pruner.js +392 -0
- package/src/task_registry.js +1357 -0
- package/src/telemetry.js +638 -0
- package/src/tools.js +997 -0
- package/src/wsl_bridge.js +629 -0
- package/src/wsl_env.js +171 -0
- package/stream_proxy.js +453 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action-Hash Loop Detector (Fingerprint-based Stagnation Tripwire).
|
|
3
|
+
*
|
|
4
|
+
* Implements sliding-window action-state fingerprinting to detect recurring,
|
|
5
|
+
* non-mutating tool-call cycles ("doom loops") early (e.g. turn 10-15) rather
|
|
6
|
+
* than waiting for an arbitrary global turn ceiling.
|
|
7
|
+
*
|
|
8
|
+
* Distinguishes:
|
|
9
|
+
* - Legitimate empirical exploration: changing probe scripts, testing different
|
|
10
|
+
* parameters, varying tools -> hashes differ -> NOT a loop.
|
|
11
|
+
* - True doom loop: executing the exact same tool with identical arguments and
|
|
12
|
+
* identical status multiple times without any intervening state mutations.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_LOOP_DETECTION_WINDOW,
|
|
18
|
+
DEFAULT_LOOP_DETECTION_REPETITIONS,
|
|
19
|
+
} from "../config.js";
|
|
20
|
+
|
|
21
|
+
export class LoopDetector {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} [options]
|
|
24
|
+
* @param {number} [options.windowSize] Sliding window size in turns (default: 6)
|
|
25
|
+
* @param {number} [options.threshold] Repetition count to trigger loop (default: 3)
|
|
26
|
+
* @param {number} [options.repetitionThreshold] Alias for threshold
|
|
27
|
+
*/
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.windowSize = options.windowSize || DEFAULT_LOOP_DETECTION_WINDOW || 6;
|
|
30
|
+
this.threshold =
|
|
31
|
+
options.threshold ||
|
|
32
|
+
options.repetitionThreshold ||
|
|
33
|
+
DEFAULT_LOOP_DETECTION_REPETITIONS ||
|
|
34
|
+
3;
|
|
35
|
+
this.repetitionThreshold = this.threshold;
|
|
36
|
+
this.history = [];
|
|
37
|
+
this.mutationsSinceLastAction = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Record that a file or workspace state mutation occurred (e.g. edit_file, write_file, apply_patch).
|
|
42
|
+
*/
|
|
43
|
+
recordMutation() {
|
|
44
|
+
this.mutationsSinceLastAction++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compute a deterministic hash for a tool call.
|
|
49
|
+
* Normalizes argument object keys so formatting differences don't mask identical actions.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} toolName
|
|
52
|
+
* @param {object|string} args
|
|
53
|
+
* @param {object} [result]
|
|
54
|
+
* @returns {string} 16-character hex signature
|
|
55
|
+
*/
|
|
56
|
+
computeActionHash(toolName, args, result) {
|
|
57
|
+
let canonicalArgs = "";
|
|
58
|
+
try {
|
|
59
|
+
if (args && typeof args === "object") {
|
|
60
|
+
canonicalArgs = JSON.stringify(args, Object.keys(args).sort());
|
|
61
|
+
} else {
|
|
62
|
+
canonicalArgs = String(args ?? "");
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
canonicalArgs = String(args ?? "");
|
|
66
|
+
}
|
|
67
|
+
const exitSig = result ? (result.exitCode ?? (result.isError ? 1 : 0)) : 0;
|
|
68
|
+
return crypto
|
|
69
|
+
.createHash("sha256")
|
|
70
|
+
.update(`${toolName}:${canonicalArgs}:${exitSig}`)
|
|
71
|
+
.digest("hex")
|
|
72
|
+
.slice(0, 16);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Record an executed action and evaluate loop status over the sliding window.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} toolName
|
|
79
|
+
* @param {object|string} args
|
|
80
|
+
* @param {object} [result]
|
|
81
|
+
* @returns {{ isLoop: boolean, repeats: number, repetitionCount: number, fingerprint: string, signature: string, toolName: string }}
|
|
82
|
+
*/
|
|
83
|
+
recordAction(toolName, args, result) {
|
|
84
|
+
const hash = this.computeActionHash(toolName, args, result);
|
|
85
|
+
const hadMutation = this.mutationsSinceLastAction > 0;
|
|
86
|
+
this.history.push({
|
|
87
|
+
toolName,
|
|
88
|
+
hash,
|
|
89
|
+
hadMutation,
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
});
|
|
92
|
+
this.mutationsSinceLastAction = 0;
|
|
93
|
+
|
|
94
|
+
while (this.history.length > this.windowSize) {
|
|
95
|
+
this.history.shift();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.checkLoop();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if the latest action is repeating within the sliding window without intervening mutations.
|
|
103
|
+
*
|
|
104
|
+
* @returns {{ isLoop: boolean, repeats: number, repetitionCount: number, fingerprint: string, signature: string, toolName: string }}
|
|
105
|
+
*/
|
|
106
|
+
checkLoop() {
|
|
107
|
+
if (this.history.length === 0) {
|
|
108
|
+
return { isLoop: false, repeats: 0, repetitionCount: 0, fingerprint: "", signature: "", toolName: "" };
|
|
109
|
+
}
|
|
110
|
+
const latest = this.history[this.history.length - 1];
|
|
111
|
+
let matchCount = 0;
|
|
112
|
+
let mutationsInBetween = false;
|
|
113
|
+
|
|
114
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
115
|
+
const item = this.history[i];
|
|
116
|
+
if (item.hash === latest.hash) {
|
|
117
|
+
matchCount++;
|
|
118
|
+
}
|
|
119
|
+
if (item.hadMutation) {
|
|
120
|
+
// Mutation occurred before/at this action; do not count actions before it
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isLoop = matchCount >= this.threshold;
|
|
126
|
+
return {
|
|
127
|
+
isLoop,
|
|
128
|
+
repeats: matchCount,
|
|
129
|
+
repetitionCount: matchCount,
|
|
130
|
+
fingerprint: latest.hash,
|
|
131
|
+
signature: latest.hash,
|
|
132
|
+
toolName: latest.toolName,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|